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:
@@ -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())
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user