Move plot_to_data_uri to lib/colored_graphs and rename to plot_colored_graph_to_data_uri

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 11:18:23 -04:00
parent 07ad553568
commit 49f456e467
4 changed files with 42 additions and 60 deletions
+3 -40
View File
@@ -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/<seq_id>. 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)
+16
View File
@@ -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.
-19
View File
@@ -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
+23 -1
View File
@@ -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/<seq_id>. 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