diff --git a/colored_pentagon_contractions.py b/colored_pentagon_contractions.py index bce6153..5c75d24 100644 --- a/colored_pentagon_contractions.py +++ b/colored_pentagon_contractions.py @@ -1,17 +1,13 @@ """Example: colored pentagon reduction on a random 20-vertex triangulation.""" -import json -import base64 from collections import defaultdict from pathlib import Path from typing import Any, cast, TypedDict, Literal from sage.all import graphs, Graph # type: ignore[attr-defined] # pylint: disable=no-name-in-module -from lib.tutte_embedding import tutte_embedding -from lib.planar_embedding import outer_face, get_embedding_from_pos +from lib.planar_embedding import get_embedding_from_pos from lib.colored_graphs import ColoredGraphId, VertexColoring, save_colored_graph -from lib.operations import Operation, operation_to_string, operation_sequence_id +from lib.operations import Operation, save_operation_sequence DIR = Path(__file__).parent -PALETTE = ['red', 'blue', 'green', 'yellow'] def _neighbors_form_cycle(g: Graph, v: Any) -> bool: """Return True if the neighbors of v induce a cycle in g.""" @@ -134,38 +130,5 @@ starting_coloring = {v: i for i, cls in enumerate(starting_coloring_classes) for _, _, initial_canon_id = save_colored_graph(G, starting_coloring) G.is_planar(set_embedding=True, set_pos=True) -def strip_graphs(obj: Any) -> Any: - if isinstance(obj, dict): - return {k: strip_graphs(v) for k, v in obj.items() if not isinstance(v, Graph)} - if isinstance(obj, list): - return [strip_graphs(v) for v in obj] - return obj - -def plot_to_data_uri(g: Graph, coloring: VertexColoring) -> str: - import tempfile - vertex_colors: defaultdict[str, list[Any]] = defaultdict(list) - for v, c in coloring.items(): - vertex_colors[PALETTE[c]].append(v) - if g.get_pos() is None: - g.is_planar(set_embedding=True, set_pos=True) - g.set_pos(tutte_embedding(g, outer_face(g))) - with tempfile.NamedTemporaryFile(suffix='.png', delete=True) as f: - g.plot(vertex_colors=dict(vertex_colors)).save(f.name) - png_bytes = Path(f.name).read_bytes() - return f"data:image/png;base64,{base64.b64encode(png_bytes).decode()}" - -def save_operation_sequence(op_sequence: list[ReduceOperation], g: Graph, coloring: VertexColoring) -> str: - """Save op_sequence as JSON and Markdown under data/operations/. Returns the sequence id.""" - op_seq_id = operation_sequence_id(op_sequence) - op_dir = DIR / "data" / "operations" / op_seq_id - op_dir.mkdir(parents=True, exist_ok=True) - (op_dir / "colored_pentagon_contractions.json").write_text(json.dumps(strip_graphs(op_sequence), indent=2)) - md_lines = [f"## start\n\n![start]({plot_to_data_uri(g, coloring)})"] - for op in op_sequence: - meta_json = json.dumps(op['meta']) - md_lines.append(f"## {op['name']} {meta_json}\n\n![b]({plot_to_data_uri(op['result_graph'], op['result_coloring'])})") - (op_dir / "colored_pentagon_contractions.md").write_text("\n".join(md_lines) + "\n") - return op_seq_id - op_sequence = reduce(G, starting_coloring, initial_canon_id) -save_operation_sequence(op_sequence, G, starting_coloring) +save_operation_sequence(op_sequence, G, starting_coloring, DIR) diff --git a/lib/colored_graphs.py b/lib/colored_graphs.py index a9a841c..f38709b 100644 --- a/lib/colored_graphs.py +++ b/lib/colored_graphs.py @@ -1,10 +1,13 @@ """Utilities for canonizing and saving colored graphs.""" import base64 +import tempfile from collections import defaultdict from pathlib import Path from typing import Any, cast, TypedDict from sage.all import Graph, save, load # type: ignore[attr-defined] # pylint: disable=no-name-in-module +from lib.tutte_embedding import tutte_embedding +from lib.planar_embedding import outer_face DIR = Path(__file__).parent.parent PALETTE = ['red', 'blue', 'green', 'yellow'] @@ -37,6 +40,19 @@ def canonize_colored_graph(g: Graph, coloring: VertexColoring) -> tuple[Graph, V return canonical, canonical_coloring, ColoredGraphId(graph_id=graph_id, coloring_id=coloring_id) +def plot_colored_graph_to_data_uri(g: Graph, coloring: VertexColoring) -> str: + vertex_colors: defaultdict[str, list[Any]] = defaultdict(list) + for v, c in coloring.items(): + vertex_colors[PALETTE[c]].append(v) + if g.get_pos() is None: + g.is_planar(set_embedding=True, set_pos=True) + g.set_pos(tutte_embedding(g, outer_face(g))) + with tempfile.NamedTemporaryFile(suffix='.png', delete=True) as f: + g.plot(vertex_colors=dict(vertex_colors)).save(f.name) + png_bytes = Path(f.name).read_bytes() + return f"data:image/png;base64,{base64.b64encode(png_bytes).decode()}" + + def save_colored_graph(g: Graph, coloring: VertexColoring) -> tuple[Graph, VertexColoring, ColoredGraphId]: """ Canonize g and coloring, save to disk, and return the canonical forms with id. diff --git a/lib/coloring.py b/lib/coloring.py deleted file mode 100644 index fbbf1bb..0000000 --- a/lib/coloring.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Utilities for converting integer colorings to named colors.""" -from typing import Any - -COLORS = ['blue', 'red', 'green', 'yellow', 'purple'] - -def convert_coloring( - coloring: dict[Any, int], - vertices: list[Any] | None = None -) -> dict[str, list[Any]]: - """Convert an integer coloring dict to a dict mapping color names to vertex lists.""" - colors: dict[str, list[Any]] = {} - for k, v in coloring.items(): - if vertices and k not in vertices: - continue - color = COLORS[v] - if color not in colors: - colors[color] = [] - colors[color].append(k) - return colors diff --git a/lib/operations.py b/lib/operations.py index e1a5094..1f19cc1 100644 --- a/lib/operations.py +++ b/lib/operations.py @@ -1,7 +1,9 @@ import hashlib +import json +from pathlib import Path from typing import Any, TypedDict from sage.all import Graph # type: ignore[attr-defined] # pylint: disable=no-name-in-module -from lib.colored_graphs import ColoredGraphId, VertexColoring +from lib.colored_graphs import ColoredGraphId, VertexColoring, plot_colored_graph_to_data_uri class Operation(TypedDict): """Information about a change made to a (colored) graph""" @@ -22,3 +24,23 @@ def operation_to_string(op: Operation) -> str: def operation_sequence_id(ops: list[Operation]) -> str: joined = "\n".join(operation_to_string(op) for op in ops) return hashlib.sha256(joined.encode()).hexdigest() + +def strip_graphs(obj: Any) -> Any: + if isinstance(obj, dict): + return {k: strip_graphs(v) for k, v in obj.items() if not isinstance(v, Graph)} + if isinstance(obj, list): + return [strip_graphs(v) for v in obj] + return obj + +def save_operation_sequence(op_sequence: list[Operation], g: Graph, coloring: VertexColoring, save_dir: Path) -> str: + """Save op_sequence as JSON and Markdown under save_dir/data/operations/. Returns the sequence id.""" + op_seq_id = operation_sequence_id(op_sequence) + op_dir = save_dir / "data" / "operations" / op_seq_id + op_dir.mkdir(parents=True, exist_ok=True) + (op_dir / "colored_pentagon_contractions.json").write_text(json.dumps(strip_graphs(op_sequence), indent=2)) + md_lines = [f"## start\n\n![start]({plot_colored_graph_to_data_uri(g, coloring)})"] + for op in op_sequence: + meta_json = json.dumps(op['meta']) + md_lines.append(f"## {op['name']} {meta_json}\n\n![b]({plot_colored_graph_to_data_uri(op['result_graph'], op['result_coloring'])})") + (op_dir / "colored_pentagon_contractions.md").write_text("\n".join(md_lines) + "\n") + return op_seq_id