diff --git a/colored_pentagon_reduction/example.py b/colored_pentagon_reduction/example.py index aad3b6f..91267b9 100644 --- a/colored_pentagon_reduction/example.py +++ b/colored_pentagon_reduction/example.py @@ -1,93 +1,121 @@ -from sage.all import graphs, Graph +"""Example: colored pentagon reduction on a random 20-vertex triangulation.""" +import base64 from collections import defaultdict from pathlib import Path -import base64 +from typing import Any, cast + +from sage.all import graphs, Graph # type: ignore[attr-defined] # pylint: disable=no-name-in-module DIR = Path(__file__).parent - PALETTE = ['red', 'blue', 'green', 'yellow'] -def plot_colored(g, coloring, title, filename): +VertexColoring = dict[Any, Any] + + +def plot_colored(g: Graph, coloring: VertexColoring, title: str, filename: str) -> None: + """ + Save a plot of g with vertices colored in a file according to it's + graph canonization and coloring + """ g.is_planar(set_embedding=True, set_pos=True) - vertex_colors = defaultdict(list) + vertex_colors: defaultdict[str, list[Any]] = defaultdict(list) for v, c in coloring.items(): vertex_colors[PALETTE[c]].append(v) - label = base64.urlsafe_b64encode(g.canonical_label().graph6_string().encode()).decode() + canonical = cast(Graph, g.canonical_label()) + label = base64.urlsafe_b64encode( + canonical.graph6_string().encode() + ).decode() out_dir = DIR / "data" / label out_dir.mkdir(exist_ok=True) g.plot(vertex_colors=dict(vertex_colors), title=title).save(out_dir / filename) -def pluck(G: Graph, coloring, v0, kind, step=1): - # Contract v1 and v2 into v0 to obtain the minor G'. - # merge_vertices([v0, v1, v2]) folds v1 and v2 into v0. - G_prime = G.copy() - G_prime.delete_vertex(v0) + +def _neighbors_form_cycle(g: Graph, v: Any) -> bool: + """Return True if the neighbors of v induce a cycle in g.""" + return bool(cast(Graph, g.subgraph(g.neighbors(v))).is_cycle()) + +def pluck( + g: Graph, + coloring: VertexColoring, + v0: Any, + kind: str, + step: int = 1 +) -> tuple[Graph, VertexColoring]: + """Delete v0 from g and recurse.""" + g_prime = g.copy() + g_prime.delete_vertex(v0) coloring_prime = coloring.copy() del coloring_prime[v0] - print(f"\nG' (after reduction): {G_prime.order()} vertices, {G_prime.size()} edges") + print(f"\nG' (after pluck): {g_prime.order()} vertices, {g_prime.size()} edges") + plot_colored( + g_prime, coloring_prime, + f"G' (after pluck for v0={v0})", + f"step_{step:04d}_({kind}).png", + ) + return g_prime, coloring_prime - # Plot before and after side by side. - plot_colored(G_prime, coloring_prime, f"G' (after pluck for v0={v0})", f"step_{step:04d}_({kind}).png") - return reduce(G_prime, coloring_prime, step+1) -def squish(G: Graph, coloring, v0, kind, step=1): - # Among v0's neighbors, find two with the same color (v1 and v2). - neighbor_by_color = defaultdict(list) - for v in G.neighbors(v0): +def squish( + g: Graph, + coloring: VertexColoring, + v0: Any, kind: str, + step: int = 1 +) -> tuple[Graph, VertexColoring]: + """Contract two same-colored neighbors of v0 into v0 and recurse.""" + neighbor_by_color: defaultdict[Any, list[Any]] = defaultdict(list) + for v in g.neighbors(v0): neighbor_by_color[coloring[v]].append(v) v1, v2 = next( (vs[0], vs[1]) for vs in neighbor_by_color.values() if len(vs) >= 2 ) - print(f"Shared-color neighbors: v1 = {v1}, v2 = {v2} (color {coloring[v1]})") + print(f"Shared-color neighbors: v1={v1}, v2={v2} (color {coloring[v1]})") - # Contract v1 and v2 into v0 to obtain the minor G'. - # merge_vertices([v0, v1, v2]) folds v1 and v2 into v0. - G_prime = G.copy() - G_prime.merge_vertices([v0, v1, v2]) + g_prime = g.copy() + g_prime.merge_vertices([v0, v1, v2]) coloring_prime = {v: c for v, c in coloring.items() if v not in (v1, v2)} coloring_prime[v0] = coloring[v1] - print(f"\nG' (after reduction): {G_prime.order()} vertices, {G_prime.size()} edges") + print(f"\nG' (after squish): {g_prime.order()} vertices, {g_prime.size()} edges") + plot_colored( + g_prime, coloring_prime, + f"G' (after squish for v0={v0}, v1={v1}, v2={v2})", + f"step_{step:04d}_({kind}).png", + ) + return g_prime, coloring_prime - # Plot before and after side by side. - plot_colored(G_prime, coloring_prime, f"G' (after squish for v0={v0}, v1={v1}, v2={v2})", f"step_{step:04d}_({kind}).png") - return reduce(G_prime, coloring_prime, step+1) -def reduce(G, coloring, step=1): - # 2. Find a proper 4-coloring. - # G.coloring() returns a partition of vertices into color classes. - # By pigeonhole (5 neighbors, at most 3 available colors for each - # degree-5 vertex), the reduction step below always succeeds. +def reduce(g: Graph, coloring: VertexColoring, step: int = 1) -> None: + """Repeatedly apply pluck/squish reductions until no candidates remain.""" print(f"Coloring: {coloring}") - degree_4_candidates = [] - degree_5_candidates = [] + degree_4_candidates: list[Any] = [] + degree_5_candidates: list[Any] = [] - # Pick the first degree 5 vertex where the neighbors form a wheel - for v in G.vertices(): - if G.degree(v) == 3 and G.subgraph(G.neighbors(v)).is_cycle(): - return pluck(G, coloring, v, 'triangle', step) - elif G.degree(v) == 4 and G.subgraph(G.neighbors(v)).is_cycle(): + for v in g.vertices(): + if g.degree(v) == 3 and _neighbors_form_cycle(g, v): + g_prime, coloring_prime = pluck(g, coloring, v, 'triangle', step) + return reduce(g_prime, coloring_prime, step + 1) + if g.degree(v) == 4 and _neighbors_form_cycle(g, v): degree_4_candidates.append(v) - elif G.degree(v) == 5 and G.subgraph(G.neighbors(v)).is_cycle(): + elif g.degree(v) == 5 and _neighbors_form_cycle(g, v): degree_5_candidates.append(v) - for v in degree_4_candidates: - return squish(G, coloring, v, 'square', step) + if degree_4_candidates: + g_prime, coloring_prime = squish(g, coloring, degree_4_candidates[0], 'square', step) + return reduce(g_prime, coloring_prime, step + 1) - for v in degree_5_candidates: - return squish(G, coloring, v, 'triangle', step) + if degree_5_candidates: + g_prime, coloring_prime = squish(g, coloring, degree_5_candidates[0], 'triangle', step) + return reduce(g_prime, coloring_prime, step + 1) print("DONE") - return -# 1. Generate a maximal planar graph (triangulation) on 14 vertices -# with minimum degree 5, via plantri. + G = next(graphs.planar_graphs(20, minimum_degree=5)) print(f"G: {G.order()} vertices, {G.size()} edges") print(f"Degree sequence: {sorted(G.degree_sequence(), reverse=True)}") -coloring_classes = G.coloring() -coloring = {v: i for i, cls in enumerate(coloring_classes) for v in cls} -plot_colored(G, coloring, "Start", f"step_{0:04d}.png") +starting_coloring_classes = G.coloring() +starting_coloring = {v: i for i, cls in enumerate(starting_coloring_classes) for v in cls} +plot_colored(G, starting_coloring, "Start", f"step_{0:04d}.png") -reduce(G, coloring) \ No newline at end of file +reduce(G, starting_coloring) diff --git a/pyrightconfig.json b/pyrightconfig.json index c7a67a5..13d48af 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,7 +1,7 @@ { - "reportUnknownMemberType": "warning", - "reportUnknownArgumentType": "warning", + "reportUnknownMemberType": "none", + "reportUnknownArgumentType": "none", "reportUnknownParameterType": "warning", "reportMissingParameterType": "warning", - "reportUnknownVariableType": "warning" + "reportUnknownVariableType": "none" } diff --git a/run.sh b/run.sh index 5a22a09..69467de 100755 --- a/run.sh +++ b/run.sh @@ -66,8 +66,9 @@ run_sage() { } lint() { - "$VENV_PYTHON" -m pyright lib/ --pythonpath "$SAGE_PYTHON_PATH" - "$VENV_PYTHON" -m pylint lib/ \ + local path="${1:-lib/}" + "$VENV_PYTHON" -m pyright "$path" --pythonpath "$SAGE_PYTHON_PATH" + "$VENV_PYTHON" -m pylint "$path" \ --init-hook="import sys; sys.path.insert(0, '${SAGE_SITE_PACKAGES}'); sys.path.insert(0, '${SCRIPT_DIR}')" \ --disable=fixme } @@ -111,6 +112,9 @@ _run_sh() { sage) _files ;; + lint) + _files + ;; completion) _values 'shell' 'zsh' ;; @@ -143,7 +147,8 @@ sage) run_sage "$@" ;; lint) - lint + shift + lint "$@" ;; completion) shift