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.""" """Example: colored pentagon reduction on a random 20-vertex triangulation."""
import json
import base64
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
from typing import Any, cast, TypedDict, Literal 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 # type: ignore[attr-defined] # pylint: disable=no-name-in-module
from lib.tutte_embedding import tutte_embedding from lib.planar_embedding import get_embedding_from_pos
from lib.planar_embedding import outer_face, get_embedding_from_pos
from lib.colored_graphs import ColoredGraphId, VertexColoring, save_colored_graph 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 DIR = Path(__file__).parent
PALETTE = ['red', 'blue', 'green', 'yellow']
def _neighbors_form_cycle(g: Graph, v: Any) -> bool: def _neighbors_form_cycle(g: Graph, v: Any) -> bool:
"""Return True if the neighbors of v induce a cycle in g.""" """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) _, _, initial_canon_id = save_colored_graph(G, starting_coloring)
G.is_planar(set_embedding=True, set_pos=True) 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) 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.""" """Utilities for canonizing and saving colored graphs."""
import base64 import base64
import tempfile
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
from typing import Any, cast, TypedDict from typing import Any, cast, TypedDict
from sage.all import Graph, save, load # type: ignore[attr-defined] # pylint: disable=no-name-in-module 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 DIR = Path(__file__).parent.parent
PALETTE = ['red', 'blue', 'green', 'yellow'] 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) 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]: 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. 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 hashlib
import json
from pathlib import Path
from typing import Any, TypedDict from typing import Any, TypedDict
from sage.all import Graph # type: ignore[attr-defined] # pylint: disable=no-name-in-module 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): class Operation(TypedDict):
"""Information about a change made to a (colored) graph""" """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: def operation_sequence_id(ops: list[Operation]) -> str:
joined = "\n".join(operation_to_string(op) for op in ops) joined = "\n".join(operation_to_string(op) for op in ops)
return hashlib.sha256(joined.encode()).hexdigest() 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