From 156f76c3958a24bbdfbb35033df85b66fff59d47 Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 20 Apr 2026 18:43:26 -0400 Subject: [PATCH] Move data output to root data/ symlink and gitignore generated files Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 2 + colored_pentagon_reduction/example.py | 183 +++++++++++++++++++------- 2 files changed, 134 insertions(+), 51 deletions(-) diff --git a/.gitignore b/.gitignore index b316ddd..3bad7c4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ .env.* .venv/ .vscode/ +data +colored_pentagon_reduction/data/ diff --git a/colored_pentagon_reduction/example.py b/colored_pentagon_reduction/example.py index 91267b9..f896268 100644 --- a/colored_pentagon_reduction/example.py +++ b/colored_pentagon_reduction/example.py @@ -2,66 +2,117 @@ import base64 from collections import defaultdict from pathlib import Path -from typing import Any, cast +from typing import Any, cast, TypedDict, Literal -from sage.all import graphs, Graph # type: ignore[attr-defined] # pylint: disable=no-name-in-module +from sage.all import graphs, Graph, save, load # type: ignore[attr-defined] # pylint: disable=no-name-in-module -DIR = Path(__file__).parent +DIR = Path(__file__).parent.parent PALETTE = ['red', 'blue', 'green', 'yellow'] VertexColoring = dict[Any, Any] +class ColoredGraphId(TypedDict): + """Canonical id representing a colored graph""" + graph_id: str + coloring_id: str -def plot_colored(g: Graph, coloring: VertexColoring, title: str, filename: str) -> None: +class Operation(TypedDict): + """Information about a change made to a (colored) graph""" + name: Any + meta: Any + before: ColoredGraphId + after: ColoredGraphId + +class CanonicalColoredGraph(TypedDict): + """Canonical representation of a colored graph""" + colored_graph_id: ColoredGraphId + graph: Graph + coloring: VertexColoring + +def canonize_colored_graph(g: Graph, coloring: VertexColoring) -> ColoredGraphId: + """Mutate g and coloring to canonical labels and return a canonical ColoredGraphId""" + canonical, cert = cast( + tuple[Graph, dict[Any, int]], + g.canonical_label(certificate=True), + ) + graph_id = base64.urlsafe_b64encode( + canonical.graph6_string().encode() + ).decode() + + color_seq = [0] * g.order() + for orig_v, canon_idx in cert.items(): + color_seq[canon_idx] = coloring[orig_v] + + coloring.clear() + for canon_idx, color in enumerate(color_seq): + coloring[canon_idx] = color + coloring_id = base64.urlsafe_b64encode(bytes(color_seq)).decode() + g.relabel(cert) + return ColoredGraphId(graph_id=graph_id, coloring_id=coloring_id) + +def save_colored_graph(g: Graph, coloring: VertexColoring) -> tuple[Graph, VertexColoring, ColoredGraphId]: """ - Save a plot of g with vertices colored in a file according to it's - graph canonization and coloring + Relabel g and coloring into canonical form, save to disk, and return both. + If already saved, load and return the cached graph. """ + cid = canonize_colored_graph(g, coloring) + out_dir = DIR / "data" / cid['graph_id'] / cid['coloring_id'] + if (out_dir / "graph.sobj").exists(): + g_canon = cast(Graph, load(str(out_dir / 'graph'))) + return g_canon, coloring, cid g.is_planar(set_embedding=True, set_pos=True) vertex_colors: defaultdict[str, list[Any]] = defaultdict(list) for v, c in coloring.items(): vertex_colors[PALETTE[c]].append(v) - 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) - + out_dir.mkdir(parents=True, exist_ok=True) + g.plot( + vertex_colors=dict(vertex_colors), + title=f"graph: {cid['graph_id']} coloring: {cid['coloring_id']}", + ).save(out_dir / 'graph.png') + save(g, str(out_dir / 'graph')) + return g, coloring, cid 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.""" +class PluckMeta(TypedDict): + """Meta information about the pluck operation""" + v0: Any + +class PluckOperation(Operation): + """Info about an operation in which a vertex v0 and its incident edges is removed from G""" + name: Literal['pluck'] + meta: PluckMeta + +def pluck(g: Graph, coloring: VertexColoring, v0: Any) -> tuple[Graph, VertexColoring]: + """Delete v0 and all its incident edges from g""" g_prime = g.copy() g_prime.delete_vertex(v0) coloring_prime = coloring.copy() del coloring_prime[v0] - 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 -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.""" +class SquishMeta(TypedDict): + """Meta information about the squish operation""" + v0: Any + v_merged: set[Any] + +class SquishOperation(Operation): + """ + Info about an operation in which two same colored neighbors of a vertex v0 are merged + into v0 + """ + name: Literal['squish'] + meta: SquishMeta + +def squish(g: Graph, coloring: VertexColoring, v0: Any) -> tuple[Graph, VertexColoring, Any, Any]: + """ + Contract two same-colored neighbors of v0 into v0 and return a new valid + coloring along with the new graph. + NOTE: assumes g is a maximal planar graph + """ neighbor_by_color: defaultdict[Any, list[Any]] = defaultdict(list) for v in g.neighbors(v0): neighbor_by_color[coloring[v]].append(v) @@ -69,23 +120,36 @@ def squish( 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]})") 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 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 + return g_prime, coloring_prime, v1, v2 -def reduce(g: Graph, coloring: VertexColoring, step: int = 1) -> None: +Step = tuple[str, str] # (name, title) + +def reduction_operation_to_string(op: SquishOperation | PluckOperation): + """String representation of the given operation""" + if op['name'] == 'squish': + meta = op['meta'] + vm = list(sorted(op['meta']['v_merged'])) + return f"squish_(v0={meta['v0']}, v1={vm[0]}, v2={vm[1]}" + if op['name'] == 'pluck': + meta = op['meta'] + return f"pluck_(v0={meta['v0']}" + +def reduce( + g: Graph, + coloring: VertexColoring, + step: int = 1, + steps: list[Step] | None = None, +) -> list[Step]: """Repeatedly apply pluck/squish reductions until no candidates remain.""" + if steps is None: + steps = [] + print(f"Coloring: {coloring}") degree_4_candidates: list[Any] = [] @@ -93,22 +157,39 @@ def reduce(g: Graph, coloring: VertexColoring, step: int = 1) -> None: 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) + g_prime, coloring_prime = pluck(g, coloring, v) + print(f"\nG' (after pluck v0={v}): {g_prime.order()} vertices, {g_prime.size()} edges") + name, title = f"step_{step:04d}_(triangle)", f"G' (after pluck for v0={v})" + steps.append((name, title)) + save_colored_graph(g_prime, coloring_prime) + return reduce(g_prime, coloring_prime, step + 1, steps) if g.degree(v) == 4 and _neighbors_form_cycle(g, v): degree_4_candidates.append(v) elif g.degree(v) == 5 and _neighbors_form_cycle(g, v): degree_5_candidates.append(v) 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) + v0 = degree_4_candidates[0] + g_prime, coloring_prime, v1, v2 = squish(g, coloring, v0) + print(f"Shared-color neighbors: v1={v1}, v2={v2} (color {coloring[v1]})") + print(f"\nG' (after squish v0={v0}): {g_prime.order()} vertices, {g_prime.size()} edges") + name, title = f"step_{step:04d}_(square)", f"G' (after squish for v0={v0})" + steps.append((name, title)) + save_colored_graph(g_prime, coloring_prime) + return reduce(g_prime, coloring_prime, step + 1, steps) 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) + v0 = degree_5_candidates[0] + g_prime, coloring_prime, v1, v2 = squish(g, coloring, v0) + print(f"Shared-color neighbors: v1={v1}, v2={v2} (color {coloring[v1]})") + print(f"\nG' (after squish v0={v0}): {g_prime.order()} vertices, {g_prime.size()} edges") + name, title = f"step_{step:04d}_(pentagon)", f"G' (after squish for v0={v0})" + steps.append((name, title)) + save_colored_graph(g_prime, coloring_prime) + return reduce(g_prime, coloring_prime, step + 1, steps) print("DONE") + return steps G = next(graphs.planar_graphs(20, minimum_degree=5)) @@ -116,6 +197,6 @@ print(f"G: {G.order()} vertices, {G.size()} edges") print(f"Degree sequence: {sorted(G.degree_sequence(), reverse=True)}") 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") +save_colored_graph(G, starting_coloring) reduce(G, starting_coloring)