Move outer_face and get_embedding_from_pos to lib/planar_embedding

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 21:04:17 -04:00
parent 03f92494f1
commit 2cf51ecbb5
2 changed files with 52 additions and 46 deletions
+1 -46
View File
@@ -5,10 +5,9 @@ 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
import math
from sage.all import graphs, Graph, save, load # 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
from lib.tutte_embedding import tutte_embedding from lib.tutte_embedding import tutte_embedding
from lib.planar_embedding import outer_face, get_embedding_from_pos
DIR = Path(__file__).parent.parent DIR = Path(__file__).parent.parent
PALETTE = ['red', 'blue', 'green', 'yellow'] PALETTE = ['red', 'blue', 'green', 'yellow']
@@ -86,50 +85,6 @@ def save_colored_graph(g: Graph, coloring: VertexColoring) -> tuple[Graph, Verte
save(g_canon, str(out_dir / 'graph')) save(g_canon, str(out_dir / 'graph'))
return g_canon, canonical_coloring, cid return g_canon, canonical_coloring, cid
def outer_face(g: Graph) -> list[Any]:
"""Return the vertices of the outer (unbounded) face of g using its planar embedding and positions."""
pos = g.get_pos()
embedding = g.get_embedding()
if not pos or not embedding:
raise Exception("Position and embedding required to find outer face")
visited: set[tuple[Any, Any]] = set()
faces: list[list[Any]] = []
for u in g.vertices():
for v in embedding[u]:
if (u, v) not in visited:
face: list[Any] = []
cu, cv = u, v
while (cu, cv) not in visited:
visited.add((cu, cv))
face.append(cu)
neighbors = embedding[cv]
cw = neighbors[(neighbors.index(cu) + 1) % len(neighbors)]
cu, cv = cv, cw
faces.append(face)
def signed_area(face: list[Any]) -> float:
coords = [pos[v] for v in face]
n = len(coords)
return sum(
coords[i][0] * coords[(i + 1) % n][1] - coords[(i + 1) % n][0] * coords[i][1]
for i in range(n)
) / 2
return min(faces, key=signed_area)
def get_embedding_from_pos(g: Graph) -> dict[Any, list[Any]]:
"""Compute a combinatorial planar embedding by sorting each vertex's neighbors by angle."""
pos_or_none = g.get_pos()
if pos_or_none is None:
raise Exception("Cannot get embedding without position")
pos: dict[Any, Any] = pos_or_none
return {
v: sorted(g.neighbors(v), key=lambda w: math.atan2(pos[w][1] - pos[v][1], pos[w][0] - pos[v][0]))
for v in g.vertices()
}
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())
+51
View File
@@ -0,0 +1,51 @@
"""Utilities for working with planar embeddings of graphs."""
import math
from typing import Any
from sage.all import Graph # type: ignore
def outer_face(g: Graph) -> list[Any]:
"""Return the vertices of the outer (unbounded) face of g using its planar embedding and positions."""
pos = g.get_pos()
embedding = g.get_embedding()
if not pos or not embedding:
raise Exception("Position and embedding required to find outer face")
visited: set[tuple[Any, Any]] = set()
faces: list[list[Any]] = []
for u in g.vertices():
for v in embedding[u]:
if (u, v) not in visited:
face: list[Any] = []
cu, cv = u, v
while (cu, cv) not in visited:
visited.add((cu, cv))
face.append(cu)
neighbors = embedding[cv]
cw = neighbors[(neighbors.index(cu) + 1) % len(neighbors)]
cu, cv = cv, cw
faces.append(face)
def signed_area(face: list[Any]) -> float:
coords = [pos[v] for v in face]
n = len(coords)
return sum(
coords[i][0] * coords[(i + 1) % n][1] - coords[(i + 1) % n][0] * coords[i][1]
for i in range(n)
) / 2
return min(faces, key=signed_area)
def get_embedding_from_pos(g: Graph) -> dict[Any, list[Any]]:
"""Compute a combinatorial planar embedding by sorting each vertex's neighbors by angle."""
pos_or_none = g.get_pos()
if pos_or_none is None:
raise Exception("Cannot get embedding without position")
pos: dict[Any, Any] = pos_or_none
return {
v: sorted(g.neighbors(v), key=lambda w: math.atan2(pos[w][1] - pos[v][1], pos[w][0] - pos[v][0]))
for v in g.vertices()
}