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:
@@ -2,3 +2,5 @@
|
||||
.env.*
|
||||
.venv/
|
||||
.vscode/
|
||||
data
|
||||
colored_pentagon_reduction/data/
|
||||
|
||||
@@ -2,66 +2,117 @@
|
||||
import base64
|
||||
from collections import defaultdict
|
||||
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']
|
||||
|
||||
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
|
||||
graph canonization and coloring
|
||||
Relabel g and coloring into canonical form, save to disk, and return both.
|
||||
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)
|
||||
vertex_colors: defaultdict[str, list[Any]] = defaultdict(list)
|
||||
for v, c in coloring.items():
|
||||
vertex_colors[PALETTE[c]].append(v)
|
||||
canonical = cast(Graph, g.canonical_label())
|
||||
label = base64.urlsafe_b64encode(
|
||||
canonical.graph6_string().encode()
|
||||
).decode()
|
||||
out_dir = DIR / "data" / label
|
||||
out_dir.mkdir(exist_ok=True)
|
||||
g.plot(vertex_colors=dict(vertex_colors), title=title).save(out_dir / filename)
|
||||
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
g.plot(
|
||||
vertex_colors=dict(vertex_colors),
|
||||
title=f"graph: {cid['graph_id']} coloring: {cid['coloring_id']}",
|
||||
).save(out_dir / 'graph.png')
|
||||
save(g, str(out_dir / 'graph'))
|
||||
return g, coloring, cid
|
||||
|
||||
def _neighbors_form_cycle(g: Graph, v: Any) -> bool:
|
||||
"""Return True if the neighbors of v induce a cycle in g."""
|
||||
return bool(cast(Graph, g.subgraph(g.neighbors(v))).is_cycle())
|
||||
|
||||
def pluck(
|
||||
g: Graph,
|
||||
coloring: VertexColoring,
|
||||
v0: Any,
|
||||
kind: str,
|
||||
step: int = 1
|
||||
) -> tuple[Graph, VertexColoring]:
|
||||
"""Delete v0 from g and recurse."""
|
||||
class PluckMeta(TypedDict):
|
||||
"""Meta information about the pluck operation"""
|
||||
v0: Any
|
||||
|
||||
class PluckOperation(Operation):
|
||||
"""Info about an operation in which a vertex v0 and its incident edges is removed from G"""
|
||||
name: Literal['pluck']
|
||||
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.delete_vertex(v0)
|
||||
coloring_prime = coloring.copy()
|
||||
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
|
||||
|
||||
|
||||
def squish(
|
||||
g: Graph,
|
||||
coloring: VertexColoring,
|
||||
v0: Any, kind: str,
|
||||
step: int = 1
|
||||
) -> tuple[Graph, VertexColoring]:
|
||||
"""Contract two same-colored neighbors of v0 into v0 and recurse."""
|
||||
class SquishMeta(TypedDict):
|
||||
"""Meta information about the squish operation"""
|
||||
v0: Any
|
||||
v_merged: set[Any]
|
||||
|
||||
class SquishOperation(Operation):
|
||||
"""
|
||||
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)
|
||||
for v in g.neighbors(v0):
|
||||
neighbor_by_color[coloring[v]].append(v)
|
||||
@@ -69,23 +120,36 @@ def squish(
|
||||
v1, v2 = next(
|
||||
(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.merge_vertices([v0, v1, v2])
|
||||
coloring_prime = {v: c for v, c in coloring.items() if v not in (v1, v2)}
|
||||
coloring_prime[v0] = coloring[v1]
|
||||
print(f"\nG' (after squish): {g_prime.order()} vertices, {g_prime.size()} edges")
|
||||
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
|
||||
return g_prime, coloring_prime, v1, v2
|
||||
|
||||
|
||||
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."""
|
||||
if steps is None:
|
||||
steps = []
|
||||
|
||||
print(f"Coloring: {coloring}")
|
||||
|
||||
degree_4_candidates: list[Any] = []
|
||||
@@ -93,22 +157,39 @@ def reduce(g: Graph, coloring: VertexColoring, step: int = 1) -> None:
|
||||
|
||||
for v in g.vertices():
|
||||
if g.degree(v) == 3 and _neighbors_form_cycle(g, v):
|
||||
g_prime, coloring_prime = pluck(g, coloring, v, 'triangle', step)
|
||||
return reduce(g_prime, coloring_prime, step + 1)
|
||||
g_prime, coloring_prime = pluck(g, coloring, v)
|
||||
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):
|
||||
degree_4_candidates.append(v)
|
||||
elif g.degree(v) == 5 and _neighbors_form_cycle(g, v):
|
||||
degree_5_candidates.append(v)
|
||||
|
||||
if degree_4_candidates:
|
||||
g_prime, coloring_prime = squish(g, coloring, degree_4_candidates[0], 'square', step)
|
||||
return reduce(g_prime, coloring_prime, step + 1)
|
||||
v0 = degree_4_candidates[0]
|
||||
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:
|
||||
g_prime, coloring_prime = squish(g, coloring, degree_5_candidates[0], 'triangle', step)
|
||||
return reduce(g_prime, coloring_prime, step + 1)
|
||||
v0 = degree_5_candidates[0]
|
||||
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")
|
||||
return steps
|
||||
|
||||
|
||||
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)}")
|
||||
starting_coloring_classes = G.coloring()
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user