Move data output to root data/ symlink and gitignore generated files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 18:43:26 -04:00
parent 5a4022c49c
commit 156f76c395
2 changed files with 134 additions and 51 deletions
+2
View File
@@ -2,3 +2,5 @@
.env.* .env.*
.venv/ .venv/
.vscode/ .vscode/
data
colored_pentagon_reduction/data/
+132 -51
View File
@@ -2,66 +2,117 @@
import base64 import base64
from collections import defaultdict from collections import defaultdict
from pathlib import Path 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'] PALETTE = ['red', 'blue', 'green', 'yellow']
VertexColoring = dict[Any, Any] 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 Relabel g and coloring into canonical form, save to disk, and return both.
graph canonization and coloring 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) g.is_planar(set_embedding=True, set_pos=True)
vertex_colors: defaultdict[str, list[Any]] = defaultdict(list) vertex_colors: defaultdict[str, list[Any]] = defaultdict(list)
for v, c in coloring.items(): for v, c in coloring.items():
vertex_colors[PALETTE[c]].append(v) vertex_colors[PALETTE[c]].append(v)
canonical = cast(Graph, g.canonical_label()) out_dir.mkdir(parents=True, exist_ok=True)
label = base64.urlsafe_b64encode( g.plot(
canonical.graph6_string().encode() vertex_colors=dict(vertex_colors),
).decode() title=f"graph: {cid['graph_id']} coloring: {cid['coloring_id']}",
out_dir = DIR / "data" / label ).save(out_dir / 'graph.png')
out_dir.mkdir(exist_ok=True) save(g, str(out_dir / 'graph'))
g.plot(vertex_colors=dict(vertex_colors), title=title).save(out_dir / filename) return g, coloring, cid
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."""
return bool(cast(Graph, g.subgraph(g.neighbors(v))).is_cycle()) return bool(cast(Graph, g.subgraph(g.neighbors(v))).is_cycle())
def pluck( class PluckMeta(TypedDict):
g: Graph, """Meta information about the pluck operation"""
coloring: VertexColoring, v0: Any
v0: Any,
kind: str, class PluckOperation(Operation):
step: int = 1 """Info about an operation in which a vertex v0 and its incident edges is removed from G"""
) -> tuple[Graph, VertexColoring]: name: Literal['pluck']
"""Delete v0 from g and recurse.""" 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 = g.copy()
g_prime.delete_vertex(v0) g_prime.delete_vertex(v0)
coloring_prime = coloring.copy() coloring_prime = coloring.copy()
del coloring_prime[v0] 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 return g_prime, coloring_prime
def squish( class SquishMeta(TypedDict):
g: Graph, """Meta information about the squish operation"""
coloring: VertexColoring, v0: Any
v0: Any, kind: str, v_merged: set[Any]
step: int = 1
) -> tuple[Graph, VertexColoring]: class SquishOperation(Operation):
"""Contract two same-colored neighbors of v0 into v0 and recurse.""" """
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) neighbor_by_color: defaultdict[Any, list[Any]] = defaultdict(list)
for v in g.neighbors(v0): for v in g.neighbors(v0):
neighbor_by_color[coloring[v]].append(v) neighbor_by_color[coloring[v]].append(v)
@@ -69,23 +120,36 @@ def squish(
v1, v2 = next( v1, v2 = next(
(vs[0], vs[1]) for vs in neighbor_by_color.values() if len(vs) >= 2 (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 = g.copy()
g_prime.merge_vertices([v0, v1, v2]) 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 = {v: c for v, c in coloring.items() if v not in (v1, v2)}
coloring_prime[v0] = coloring[v1] coloring_prime[v0] = coloring[v1]
print(f"\nG' (after squish): {g_prime.order()} vertices, {g_prime.size()} edges") return g_prime, coloring_prime, v1, v2
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
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.""" """Repeatedly apply pluck/squish reductions until no candidates remain."""
if steps is None:
steps = []
print(f"Coloring: {coloring}") print(f"Coloring: {coloring}")
degree_4_candidates: list[Any] = [] degree_4_candidates: list[Any] = []
@@ -93,22 +157,39 @@ def reduce(g: Graph, coloring: VertexColoring, step: int = 1) -> None:
for v in g.vertices(): for v in g.vertices():
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, 'triangle', step) g_prime, coloring_prime = pluck(g, coloring, v)
return reduce(g_prime, coloring_prime, step + 1) 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): 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):
degree_5_candidates.append(v) degree_5_candidates.append(v)
if degree_4_candidates: if degree_4_candidates:
g_prime, coloring_prime = squish(g, coloring, degree_4_candidates[0], 'square', step) v0 = degree_4_candidates[0]
return reduce(g_prime, coloring_prime, step + 1) 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: if degree_5_candidates:
g_prime, coloring_prime = squish(g, coloring, degree_5_candidates[0], 'triangle', step) v0 = degree_5_candidates[0]
return reduce(g_prime, coloring_prime, step + 1) 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") print("DONE")
return steps
G = next(graphs.planar_graphs(20, minimum_degree=5)) 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)}") 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}
plot_colored(G, starting_coloring, "Start", f"step_{0:04d}.png") save_colored_graph(G, starting_coloring)
reduce(G, starting_coloring) reduce(G, starting_coloring)