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