Convert between NetworkX graphs and half-edge graph representations.
The main entry point is :func:EHEG_from_nx, which embeds an undirected
planar :class:networkx.Graph (with vertex positions) into an
:class:~pleat.half.EuclideanPositionHEG. Faces are recovered from the
planar embedding implied by the cyclic angular order of edges around each
vertex; the unbounded outer face is detected as the unique negatively-
oriented face and removed.
EHEG_from_nx
EHEG_from_nx(
nxg: Graph,
positions: dict | None = None,
return_v_lookup: bool = False,
) -> "EuclideanPositionHEG | tuple[EuclideanPositionHEG, dict]"
Convert a planar undirected :class:networkx.Graph to an :class:EuclideanPositionHEG.
Parameters:
| Name |
Type |
Description |
Default |
nxg
|
Graph
|
An undirected, planar graph. Dangling vertices (degree 1) are
pruned with a warning; node and edge attributes are copied onto
the resulting :class:Vertex / :class:HalfEdge objects.
|
required
|
positions
|
dict | None
|
Optional {node: 2d position} mapping. Defaults to
interpreting each node n as np.array(n).
|
None
|
return_v_lookup
|
bool
|
If True, also return {nx_node: Vertex}.
|
False
|
Returns:
| Type |
Description |
'EuclideanPositionHEG | tuple[EuclideanPositionHEG, dict]'
|
The resulting graph, or (graph, v_lookup) if return_v_lookup is True.
|
Source code in pleat/conversions.py
| def EHEG_from_nx(
nxg: nx.Graph,
positions: dict | None = None,
return_v_lookup: bool = False,
) -> "EuclideanPositionHEG | tuple[EuclideanPositionHEG, dict]":
"""Convert a planar undirected :class:`networkx.Graph` to an :class:`EuclideanPositionHEG`.
Args:
nxg: An undirected, planar graph. Dangling vertices (degree 1) are
pruned with a warning; node and edge attributes are copied onto
the resulting :class:`Vertex` / :class:`HalfEdge` objects.
positions: Optional ``{node: 2d position}`` mapping. Defaults to
interpreting each node ``n`` as ``np.array(n)``.
return_v_lookup: If True, also return ``{nx_node: Vertex}``.
Returns:
The resulting graph, or ``(graph, v_lookup)`` if *return_v_lookup* is True.
"""
assert not nxg.is_directed()
if positions is None:
positions = {n: np.array(n) for n in nxg.nodes()}
assert isinstance(positions, dict)
n_dangling = _delete_dangling_edges_nx(nxg)
if n_dangling > 0:
logger.warning("Deleted %d dangling edges in conversion to EHG", n_dangling)
result = EuclideanPositionHEG()
v_lookup = dict()
for n, attrs in nxg.nodes().data():
v = Vertex()
# assign node attributes
for key, value in attrs.items():
v[key] = value
v["pos"] = positions[n]
v_lookup[n] = v
result.add_vertices(v_lookup.values())
h_lookup = dict()
# orig, dest
for n in nxg.nodes():
v = v_lookup[n]
h_lookup[v] = dict()
for m in nxg[n]:
w = v_lookup[m]
h = HalfEdge(orig=v, dest=w)
# assign edge attributes
for key, value in nxg[n][m].items():
h[key] = value
h_lookup[v][w] = h
v.any_outgoing = h
result.add_halfedges(h_lookup[v].values())
# rev
for v in h_lookup:
for w in h_lookup[v]:
h_lookup[v][w].rev = h_lookup[w][v]
# nex, pre
for v in h_lookup:
outgoing_halfedges = list(h_lookup[v].values())
dirs = np.array([v["pos"] - h.dest["pos"] for h in outgoing_halfedges])
angles = angle_to_axis(dirs) % (2 * np.pi)
order = np.argsort(angles)
outgoing_halfedges = [outgoing_halfedges[i] for i in order]
for hrevnex, h, hprerev in rotate_by(outgoing_halfedges, (0, 1, 2)):
h.rev.nex = hrevnex
h.pre = hprerev.rev
# the faces
unassigned_edges = copy(result.halfedges)
while unassigned_edges:
h = next(iter(unassigned_edges))
f = Face(any_side=h)
result.add_face(f)
for k in f.halfedge_iter():
k.face = f
unassigned_edges.remove(k)
# print(f.area())
result.check_consistency()
# detect 'outside' faces which should be None by their orientation
# it is selected as the one with maximal negative area
# (area 0 faces might have slightly negative areas due to numerical issues)
outside_face = None
current_min_area = 0
for f in frozenset(result.faces):
area = f.area()
if area < current_min_area:
current_min_area = area
outside_face = f
assert outside_face is not None, "Could not find an outside face to delete. Are all areas 0?"
result.delete_face(outside_face)
if not return_v_lookup:
return result
else:
return result, v_lookup
|
delaunay_tiling
delaunay_tiling(
points: ndarray, prune_sliver_angle: float | None = 0.3
) -> EuclideanPositionHEG
Build a Delaunay triangulation of points as an :class:EuclideanPositionHEG.
Parameters:
| Name |
Type |
Description |
Default |
points
|
ndarray
|
(N, 2) array of 2D vertex positions.
|
required
|
prune_sliver_angle
|
float | None
|
If not None, iteratively delete border faces whose
smallest interior angle is below this threshold (in radians), along
with border vertices of order < 3. This cleans up thin sliver
triangles produced by the Delaunay triangulation of the convex hull.
Set to None to skip pruning.
|
0.3
|
Returns:
| Type |
Description |
EuclideanPositionHEG
|
The Delaunay triangulation as a half-edge graph with vertex positions.
|
Source code in pleat/conversions.py
| def delaunay_tiling(
points: np.ndarray,
prune_sliver_angle: float | None = 0.3,
) -> EuclideanPositionHEG:
"""Build a Delaunay triangulation of ``points`` as an :class:`EuclideanPositionHEG`.
Args:
points: ``(N, 2)`` array of 2D vertex positions.
prune_sliver_angle: If not None, iteratively delete border faces whose
smallest interior angle is below this threshold (in radians), along
with border vertices of order < 3. This cleans up thin sliver
triangles produced by the Delaunay triangulation of the convex hull.
Set to ``None`` to skip pruning.
Returns:
The Delaunay triangulation as a half-edge graph with vertex positions.
"""
from scipy.spatial import Delaunay
points = np.asarray(points)
assert points.ndim == 2 and points.shape[1] == 2, f"expected (N, 2) points, got {points.shape}"
triangulation = Delaunay(points)
faces = triangulation.simplices
edges = np.stack([faces, np.roll(faces, 1, axis=1)], axis=-1).reshape(-1, 2)
nx_graph = nx.Graph()
nx_graph.add_nodes_from(range(len(points)))
nx_graph.add_edges_from(edges)
G = EHEG_from_nx(nx_graph, positions={i: pos for i, pos in enumerate(points)})
if prune_sliver_angle is not None:
G.recompute_lengths_and_angles()
while True:
to_delete = [
f
for f in G.faces
if f.on_border() and np.min([h["in_angle"] for h in f.halfedge_iter()]) < prune_sliver_angle
]
to_delete += [v for v in G.vertices if v.on_border() and v.order() < 3]
if not to_delete:
break
G.delete_subset(to_delete)
return G
|