chore: SAVEPOINT

This commit is contained in:
2026-04-20 17:00:04 -04:00
parent 6d977f5e35
commit 5a4022c49c
3 changed files with 91 additions and 58 deletions
+79 -51
View File
@@ -1,39 +1,69 @@
from sage.all import graphs, Graph
"""Example: colored pentagon reduction on a random 20-vertex triangulation."""
import base64
from collections import defaultdict
from pathlib import Path
import base64
from typing import Any, cast
from sage.all import graphs, Graph # type: ignore[attr-defined] # pylint: disable=no-name-in-module
DIR = Path(__file__).parent
PALETTE = ['red', 'blue', 'green', 'yellow']
def plot_colored(g, coloring, title, filename):
VertexColoring = dict[Any, Any]
def plot_colored(g: Graph, coloring: VertexColoring, title: str, filename: str) -> None:
"""
Save a plot of g with vertices colored in a file according to it's
graph canonization and coloring
"""
g.is_planar(set_embedding=True, set_pos=True)
vertex_colors = defaultdict(list)
vertex_colors: defaultdict[str, list[Any]] = defaultdict(list)
for v, c in coloring.items():
vertex_colors[PALETTE[c]].append(v)
label = base64.urlsafe_b64encode(g.canonical_label().graph6_string().encode()).decode()
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)
def pluck(G: Graph, coloring, v0, kind, step=1):
# Contract v1 and v2 into v0 to obtain the minor G'.
# merge_vertices([v0, v1, v2]) folds v1 and v2 into v0.
G_prime = G.copy()
G_prime.delete_vertex(v0)
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."""
g_prime = g.copy()
g_prime.delete_vertex(v0)
coloring_prime = coloring.copy()
del coloring_prime[v0]
print(f"\nG' (after reduction): {G_prime.order()} vertices, {G_prime.size()} edges")
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
# Plot before and after side by side.
plot_colored(G_prime, coloring_prime, f"G' (after pluck for v0={v0})", f"step_{step:04d}_({kind}).png")
return reduce(G_prime, coloring_prime, step+1)
def squish(G: Graph, coloring, v0, kind, step=1):
# Among v0's neighbors, find two with the same color (v1 and v2).
neighbor_by_color = defaultdict(list)
for v in G.neighbors(v0):
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."""
neighbor_by_color: defaultdict[Any, list[Any]] = defaultdict(list)
for v in g.neighbors(v0):
neighbor_by_color[coloring[v]].append(v)
v1, v2 = next(
@@ -41,53 +71,51 @@ def squish(G: Graph, coloring, v0, kind, step=1):
)
print(f"Shared-color neighbors: v1={v1}, v2={v2} (color {coloring[v1]})")
# Contract v1 and v2 into v0 to obtain the minor G'.
# merge_vertices([v0, v1, v2]) folds v1 and v2 into v0.
G_prime = G.copy()
G_prime.merge_vertices([v0, v1, v2])
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 reduction): {G_prime.order()} vertices, {G_prime.size()} edges")
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
# Plot before and after side by side.
plot_colored(G_prime, coloring_prime, f"G' (after squish for v0={v0}, v1={v1}, v2={v2})", f"step_{step:04d}_({kind}).png")
return reduce(G_prime, coloring_prime, step+1)
def reduce(G, coloring, step=1):
# 2. Find a proper 4-coloring.
# G.coloring() returns a partition of vertices into color classes.
# By pigeonhole (5 neighbors, at most 3 available colors for each
# degree-5 vertex), the reduction step below always succeeds.
def reduce(g: Graph, coloring: VertexColoring, step: int = 1) -> None:
"""Repeatedly apply pluck/squish reductions until no candidates remain."""
print(f"Coloring: {coloring}")
degree_4_candidates = []
degree_5_candidates = []
degree_4_candidates: list[Any] = []
degree_5_candidates: list[Any] = []
# Pick the first degree 5 vertex where the neighbors form a wheel
for v in G.vertices():
if G.degree(v) == 3 and G.subgraph(G.neighbors(v)).is_cycle():
return pluck(G, coloring, v, 'triangle', step)
elif G.degree(v) == 4 and G.subgraph(G.neighbors(v)).is_cycle():
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)
if g.degree(v) == 4 and _neighbors_form_cycle(g, v):
degree_4_candidates.append(v)
elif G.degree(v) == 5 and G.subgraph(G.neighbors(v)).is_cycle():
elif g.degree(v) == 5 and _neighbors_form_cycle(g, v):
degree_5_candidates.append(v)
for v in degree_4_candidates:
return squish(G, coloring, v, 'square', step)
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)
for v in degree_5_candidates:
return squish(G, coloring, v, 'triangle', step)
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)
print("DONE")
return
# 1. Generate a maximal planar graph (triangulation) on 14 vertices
# with minimum degree 5, via plantri.
G = next(graphs.planar_graphs(20, minimum_degree=5))
print(f"G: {G.order()} vertices, {G.size()} edges")
print(f"Degree sequence: {sorted(G.degree_sequence(), reverse=True)}")
coloring_classes = G.coloring()
coloring = {v: i for i, cls in enumerate(coloring_classes) for v in cls}
plot_colored(G, coloring, "Start", f"step_{0:04d}.png")
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")
reduce(G, coloring)
reduce(G, starting_coloring)
+3 -3
View File
@@ -1,7 +1,7 @@
{
"reportUnknownMemberType": "warning",
"reportUnknownArgumentType": "warning",
"reportUnknownMemberType": "none",
"reportUnknownArgumentType": "none",
"reportUnknownParameterType": "warning",
"reportMissingParameterType": "warning",
"reportUnknownVariableType": "warning"
"reportUnknownVariableType": "none"
}
+8 -3
View File
@@ -66,8 +66,9 @@ run_sage() {
}
lint() {
"$VENV_PYTHON" -m pyright lib/ --pythonpath "$SAGE_PYTHON_PATH"
"$VENV_PYTHON" -m pylint lib/ \
local path="${1:-lib/}"
"$VENV_PYTHON" -m pyright "$path" --pythonpath "$SAGE_PYTHON_PATH"
"$VENV_PYTHON" -m pylint "$path" \
--init-hook="import sys; sys.path.insert(0, '${SAGE_SITE_PACKAGES}'); sys.path.insert(0, '${SCRIPT_DIR}')" \
--disable=fixme
}
@@ -111,6 +112,9 @@ _run_sh() {
sage)
_files
;;
lint)
_files
;;
completion)
_values 'shell' 'zsh'
;;
@@ -143,7 +147,8 @@ sage)
run_sage "$@"
;;
lint)
lint
shift
lint "$@"
;;
completion)
shift