Add operation tracking, graph saving, and markdown output for pentagon reduction

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 01:36:22 -04:00
parent 156f76c395
commit 54b33a7003
+46 -27
View File
@@ -1,5 +1,7 @@
"""Example: colored pentagon reduction on a random 20-vertex triangulation.""" """Example: colored pentagon reduction on a random 20-vertex triangulation."""
import base64 import base64
import hashlib
import json
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
@@ -29,6 +31,16 @@ class CanonicalColoredGraph(TypedDict):
graph: Graph graph: Graph
coloring: VertexColoring coloring: VertexColoring
def colored_graph_id_to_string(cid: ColoredGraphId) -> str:
return f"{cid['graph_id']} {cid['coloring_id']}"
def op_to_transform_id(op: Operation) -> str:
return f"{colored_graph_id_to_string(op['before'])} -> {colored_graph_id_to_string(op['after'])}"
def operation_sequence_id(ops: list[Operation]) -> str:
joined = "\n".join(op_to_transform_id(op) for op in ops)
return hashlib.sha256(joined.encode()).hexdigest()
def canonize_colored_graph(g: Graph, coloring: VertexColoring) -> ColoredGraphId: def canonize_colored_graph(g: Graph, coloring: VertexColoring) -> ColoredGraphId:
"""Mutate g and coloring to canonical labels and return a canonical ColoredGraphId""" """Mutate g and coloring to canonical labels and return a canonical ColoredGraphId"""
canonical, cert = cast( canonical, cert = cast(
@@ -56,7 +68,7 @@ def save_colored_graph(g: Graph, coloring: VertexColoring) -> tuple[Graph, Verte
If already saved, load and return the cached graph. If already saved, load and return the cached graph.
""" """
cid = canonize_colored_graph(g, coloring) cid = canonize_colored_graph(g, coloring)
out_dir = DIR / "data" / cid['graph_id'] / cid['coloring_id'] out_dir = DIR / "data" / "graphs" / cid['graph_id'] / cid['coloring_id']
if (out_dir / "graph.sobj").exists(): if (out_dir / "graph.sobj").exists():
g_canon = cast(Graph, load(str(out_dir / 'graph'))) g_canon = cast(Graph, load(str(out_dir / 'graph')))
return g_canon, coloring, cid return g_canon, coloring, cid
@@ -97,7 +109,7 @@ def pluck(g: Graph, coloring: VertexColoring, v0: Any) -> tuple[Graph, VertexCol
class SquishMeta(TypedDict): class SquishMeta(TypedDict):
"""Meta information about the squish operation""" """Meta information about the squish operation"""
v0: Any v0: Any
v_merged: set[Any] v_merged: list[Any]
class SquishOperation(Operation): class SquishOperation(Operation):
""" """
@@ -128,21 +140,12 @@ def squish(g: Graph, coloring: VertexColoring, v0: Any) -> tuple[Graph, VertexCo
return g_prime, coloring_prime, v1, v2 return g_prime, coloring_prime, v1, v2
Step = tuple[str, str] # (name, title) Step = SquishOperation | PluckOperation
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( def reduce(
g: Graph, g: Graph,
coloring: VertexColoring, coloring: VertexColoring,
before_cid: ColoredGraphId,
step: int = 1, step: int = 1,
steps: list[Step] | None = None, steps: list[Step] | None = None,
) -> list[Step]: ) -> list[Step]:
@@ -159,10 +162,9 @@ def reduce(
if g.degree(v) == 3 and _neighbors_form_cycle(g, v): if g.degree(v) == 3 and _neighbors_form_cycle(g, v):
g_prime, coloring_prime = pluck(g, coloring, v) g_prime, coloring_prime = pluck(g, coloring, v)
print(f"\nG' (after pluck v0={v}): {g_prime.order()} vertices, {g_prime.size()} edges") 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})" _, _, after_cid = save_colored_graph(g_prime, coloring_prime)
steps.append((name, title)) steps.append(PluckOperation(name='pluck', meta=PluckMeta(v0=v), before=before_cid, after=after_cid))
save_colored_graph(g_prime, coloring_prime) return reduce(g_prime, coloring_prime, after_cid, step + 1, steps)
return reduce(g_prime, coloring_prime, step + 1, steps)
if g.degree(v) == 4 and _neighbors_form_cycle(g, v): if g.degree(v) == 4 and _neighbors_form_cycle(g, v):
degree_4_candidates.append(v) degree_4_candidates.append(v)
elif g.degree(v) == 5 and _neighbors_form_cycle(g, v): elif g.degree(v) == 5 and _neighbors_form_cycle(g, v):
@@ -173,20 +175,18 @@ def reduce(
g_prime, coloring_prime, v1, v2 = squish(g, coloring, v0) g_prime, coloring_prime, v1, v2 = squish(g, coloring, v0)
print(f"Shared-color neighbors: v1={v1}, v2={v2} (color {coloring[v1]})") 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") 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})" _, _, after_cid = save_colored_graph(g_prime, coloring_prime)
steps.append((name, title)) steps.append(SquishOperation(name='squish', meta=SquishMeta(v0=v0, v_merged=[v1, v2]), before=before_cid, after=after_cid))
save_colored_graph(g_prime, coloring_prime) return reduce(g_prime, coloring_prime, after_cid, step + 1, steps)
return reduce(g_prime, coloring_prime, step + 1, steps)
if degree_5_candidates: if degree_5_candidates:
v0 = degree_5_candidates[0] v0 = degree_5_candidates[0]
g_prime, coloring_prime, v1, v2 = squish(g, coloring, v0) g_prime, coloring_prime, v1, v2 = squish(g, coloring, v0)
print(f"Shared-color neighbors: v1={v1}, v2={v2} (color {coloring[v1]})") 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") 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})" _, _, after_cid = save_colored_graph(g_prime, coloring_prime)
steps.append((name, title)) steps.append(SquishOperation(name='squish', meta=SquishMeta(v0=v0, v_merged=[v1, v2]), before=before_cid, after=after_cid))
save_colored_graph(g_prime, coloring_prime) return reduce(g_prime, coloring_prime, after_cid, step + 1, steps)
return reduce(g_prime, coloring_prime, step + 1, steps)
print("DONE") print("DONE")
return steps return steps
@@ -197,6 +197,25 @@ print(f"G: {G.order()} vertices, {G.size()} edges")
print(f"Degree sequence: {sorted(G.degree_sequence(), reverse=True)}") print(f"Degree sequence: {sorted(G.degree_sequence(), reverse=True)}")
starting_coloring_classes = G.coloring() starting_coloring_classes = G.coloring()
starting_coloring = {v: i for i, cls in enumerate(starting_coloring_classes) for v in cls} starting_coloring = {v: i for i, cls in enumerate(starting_coloring_classes) for v in cls}
save_colored_graph(G, starting_coloring) _, _, initial_cid = save_colored_graph(G, starting_coloring)
reduce(G, starting_coloring) steps = reduce(G, starting_coloring, initial_cid)
print("\nSteps:")
print(json.dumps(steps, indent=2))
op_seq_id = operation_sequence_id(steps)
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(steps, indent=2))
def img_data_uri(cid: ColoredGraphId) -> str:
png_bytes = (DIR / "data" / "graphs" / cid['graph_id'] / cid['coloring_id'] / "graph.png").read_bytes()
return f"data:image/png;base64,{base64.b64encode(png_bytes).decode()}"
md_lines = [f"## start\n\n![start]({img_data_uri(steps[0]['before'])})"]
for step in steps:
b = step['before']
a = step['after']
meta_json = json.dumps(step['meta'])
md_lines.append(f"## {step['name']} {meta_json}\n\n![b]({img_data_uri(a)})")
(op_dir / "colored_pentagon_contractions.md").write_text("\n".join(md_lines) + "\n")