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
+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()
}