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
+80 -52
View File
@@ -1,93 +1,121 @@
from sage.all import graphs, Graph """Example: colored pentagon reduction on a random 20-vertex triangulation."""
import base64
from collections import defaultdict from collections import defaultdict
from pathlib import Path 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 DIR = Path(__file__).parent
PALETTE = ['red', 'blue', 'green', 'yellow'] 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) 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(): for v, c in coloring.items():
vertex_colors[PALETTE[c]].append(v) 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 = DIR / "data" / label
out_dir.mkdir(exist_ok=True) out_dir.mkdir(exist_ok=True)
g.plot(vertex_colors=dict(vertex_colors), title=title).save(out_dir / filename) 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'. def _neighbors_form_cycle(g: Graph, v: Any) -> bool:
# merge_vertices([v0, v1, v2]) folds v1 and v2 into v0. """Return True if the neighbors of v induce a cycle in g."""
G_prime = G.copy() return bool(cast(Graph, g.subgraph(g.neighbors(v))).is_cycle())
G_prime.delete_vertex(v0)
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() coloring_prime = coloring.copy()
del coloring_prime[v0] 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): def squish(
# Among v0's neighbors, find two with the same color (v1 and v2). g: Graph,
neighbor_by_color = defaultdict(list) coloring: VertexColoring,
for v in G.neighbors(v0): 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) neighbor_by_color[coloring[v]].append(v)
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]})") print(f"Shared-color neighbors: v1={v1}, v2={v2} (color {coloring[v1]})")
# Contract v1 and v2 into v0 to obtain the minor G'. g_prime = g.copy()
# merge_vertices([v0, v1, v2]) folds v1 and v2 into v0. 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 = {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 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): def reduce(g: Graph, coloring: VertexColoring, step: int = 1) -> None:
# 2. Find a proper 4-coloring. """Repeatedly apply pluck/squish reductions until no candidates remain."""
# 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.
print(f"Coloring: {coloring}") print(f"Coloring: {coloring}")
degree_4_candidates = [] degree_4_candidates: list[Any] = []
degree_5_candidates = [] degree_5_candidates: list[Any] = []
# Pick the first degree 5 vertex where the neighbors form a wheel 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 G.subgraph(G.neighbors(v)).is_cycle(): g_prime, coloring_prime = pluck(g, coloring, v, 'triangle', step)
return pluck(G, coloring, v, 'triangle', step) return reduce(g_prime, coloring_prime, step + 1)
elif G.degree(v) == 4 and G.subgraph(G.neighbors(v)).is_cycle(): 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 G.subgraph(G.neighbors(v)).is_cycle(): elif g.degree(v) == 5 and _neighbors_form_cycle(g, v):
degree_5_candidates.append(v) degree_5_candidates.append(v)
for v in degree_4_candidates: if degree_4_candidates:
return squish(G, coloring, v, 'square', step) 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: if degree_5_candidates:
return squish(G, coloring, v, 'triangle', step) g_prime, coloring_prime = squish(g, coloring, degree_5_candidates[0], 'triangle', step)
return reduce(g_prime, coloring_prime, step + 1)
print("DONE") 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)) G = next(graphs.planar_graphs(20, minimum_degree=5))
print(f"G: {G.order()} vertices, {G.size()} edges") 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)}")
coloring_classes = G.coloring() starting_coloring_classes = G.coloring()
coloring = {v: i for i, cls in enumerate(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, coloring, "Start", f"step_{0:04d}.png") 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", "reportUnknownMemberType": "none",
"reportUnknownArgumentType": "warning", "reportUnknownArgumentType": "none",
"reportUnknownParameterType": "warning", "reportUnknownParameterType": "warning",
"reportMissingParameterType": "warning", "reportMissingParameterType": "warning",
"reportUnknownVariableType": "warning" "reportUnknownVariableType": "none"
} }
+8 -3
View File
@@ -66,8 +66,9 @@ run_sage() {
} }
lint() { lint() {
"$VENV_PYTHON" -m pyright lib/ --pythonpath "$SAGE_PYTHON_PATH" local path="${1:-lib/}"
"$VENV_PYTHON" -m pylint 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}')" \ --init-hook="import sys; sys.path.insert(0, '${SAGE_SITE_PACKAGES}'); sys.path.insert(0, '${SCRIPT_DIR}')" \
--disable=fixme --disable=fixme
} }
@@ -111,6 +112,9 @@ _run_sh() {
sage) sage)
_files _files
;; ;;
lint)
_files
;;
completion) completion)
_values 'shell' 'zsh' _values 'shell' 'zsh'
;; ;;
@@ -143,7 +147,8 @@ sage)
run_sage "$@" run_sage "$@"
;; ;;
lint) lint)
lint shift
lint "$@"
;; ;;
completion) completion)
shift shift