"""gustaf/gustaf/edges.py.
Edges. Also known as lines.
"""
from copy import deepcopy
import numpy as np
from gustaf import helpers, settings, show, utils
from gustaf.helpers.options import Option
from gustaf.vertices import Vertices
[docs]
class EdgesShowOption(helpers.options.ShowOption):
"""
Show options for vertices.
"""
_valid_options = helpers.options.make_valid_options(
*helpers.options.vedo_common_options,
Option("vedo", "lw", "Width of edges (lines) in pixel units.", (int,)),
Option("vedo", "as_arrows", "Show edges as arrows.", (bool,)),
Option(
"vedo",
"head_radius",
"Radius of arrow head. Applicable if as_arrows is True",
(float, int),
),
Option(
"vedo",
"head_length",
"Length of arrow head. Applicable if as_arrows is True",
(float, int),
),
Option(
"vedo",
"shaft_radius",
"Radius of arrow shaft. Applicable if as_arrows is True",
(float, int),
),
)
_helps = "Edges"
def _initialize_vedo_showable(self):
"""
Initializes edges as either vedo.Lines or vedo.Arrows
Parameters
----------
None
Returns
-------
edges: vedo.Lines or vedo.Arrows
"""
if self.get("as_arrows", False):
init_options = ("head_radius", "head_length", "shaft_radius")
return show.vedo.Arrows(
self._helpee.const_vertices[self._helpee.edges],
**self[init_options],
)
else:
init_options = ("lw",)
return show.vedo.Lines(
self._helpee.const_vertices[self._helpee.edges],
**self[init_options],
)
[docs]
class Edges(Vertices):
kind = "edge"
__slots__ = (
"_edges",
"_const_edges",
)
__show_option__ = EdgesShowOption
__boundary_class__ = Vertices
def __init__(
self,
vertices=None,
edges=None,
elements=None,
copy=True,
):
"""Edges. It has vertices and edges. Also known as lines.
Parameters
-----------
vertices: (n, d) np.ndarray
edges: (n, 2) np.ndarray
"""
super().__init__(vertices=vertices, copy=copy)
if edges is not None:
self.edges = edges
elif elements is not None:
self.edges = elements
@property
def edges(self):
"""Returns edges. If edges is not its original property.
Parameters
-----------
None
Returns
--------
edges: (n, 2) np.ndarray
"""
self._logd("returning edges")
return self._edges
@edges.setter
def edges(self, es):
"""Edges setter. Similar to vertices, this is a tracked array.
Parameters
-----------
es: (n, 2) np.ndarray
Returns
--------
None
"""
self._logd("setting edges")
self._edges = helpers.data.make_tracked_array(
es, settings.INT_DTYPE, self.setter_copies
)
# shape check
if es is not None:
utils.arr.is_shape(es, (-1, 2), strict=True)
# same, but non-writeable view of tracked array
self._const_edges = self._edges.view()
self._const_edges.flags.writeable = False
@property
def const_edges(self):
"""Returns non-writeable version of edges.
Parameters
-----------
None
Returns
--------
const_edges (n, 2) np.ndarray
"""
return self._const_edges
@property
def whatami(self):
"""whatami?
Parameters
-----------
None
Returns
--------
whatami: str
"""
return "edges"
[docs]
@helpers.data.ComputedMeshData.depends_on(["elements"])
def sorted_edges(self):
"""Sort edges along axis=1.
Parameters
-----------
None
Returns
--------
edges_sorted: (n_edges, 2) np.ndarray
"""
edges = self._get_attr("edges")
return np.sort(edges, axis=1)
[docs]
@helpers.data.ComputedMeshData.depends_on(["elements"])
def unique_edges(self):
"""Returns a named tuple of unique edge info. Info includes unique
values, ids of unique edges, inverse ids, count of each unique values.
Parameters
-----------
None
Returns
--------
unique_info: Unique2DIntegers
valid attributes are {values, ids, inverse, counts}
"""
unique_info = utils.connec.sorted_unique(
self.sorted_edges(), sorted_=True
)
edges = self._get_attr("edges")
# tuple is not assignable, but entry is mutable...
unique_info.values[:] = edges[unique_info.ids]
return unique_info
[docs]
@helpers.data.ComputedMeshData.depends_on(["elements"])
def single_edges(self):
"""Returns indices of very unique edges: edges that appear only once.
For well constructed faces, this can be considered as outlines.
Parameters
-----------
None
Returns
--------
outlines: (m,) np.ndarray
"""
unique_info = self.unique_edges()
return unique_info.ids[unique_info.counts == 1]
@property
def elements(self):
"""Returns current connectivity. A short cut in FE friendly term.
Elements mean different things for different classes: Vertices ->
vertices Edges -> edges Faces -> faces Volumes -> volumes.
Parameters
-----------
None
Returns
--------
elements: (n, d) np.ndarray
int. iff elements=None
"""
elem_name = type(self).__qualname__.lower()
self._logd(f"returning {elem_name}")
return getattr(self, elem_name)
@elements.setter
def elements(self, elements):
"""Calls corresponding connectivity setter. A short cut in FEM friendly
term. Vertices -> vertices Edges -> edges Faces -> faces Volumes ->
volumes.
Parameters
-----------
elements: (n, d) np.ndarray
Returns
--------
None
"""
# naming rule in gustaf
elem_name = type(self).__qualname__.lower()
self._logd(f"Setting {elem_name}'s connectivity.")
return setattr(self, elem_name, elements)
@property
def const_elements(self):
"""Returns non-mutable version of elements.
Parameters
-----------
None
Returns
--------
non_mutable_elements: (n, d) TrackedArray
"""
self._logd("returning const_elements")
return getattr(self, "const_" + type(self).__qualname__.lower())
[docs]
@helpers.data.ComputedMeshData.depends_on(["vertices", "elements"])
def centers(self):
"""Center of elements.
Parameters
-----------
None
Returns
--------
centers: (n_elements, d) np.ndarray
"""
self._logd("computing centers")
return self.const_vertices[self.const_elements].mean(axis=1)
[docs]
@helpers.data.ComputedMeshData.depends_on(["vertices", "elements"])
def referenced_vertices(
self,
):
"""Returns mask of referenced vertices.
Parameters
-----------
None
Returns
--------
referenced: (n,) np.ndarray
"""
referenced = np.zeros(len(self.const_vertices), dtype=bool)
referenced[self.const_elements] = True
return referenced
[docs]
def remove_unreferenced_vertices(self):
"""Remove unreferenced vertices. Adapted from
`github.com/mikedh/trimesh`
Parameters
-----------
None
Returns
--------
new_self: type(self)
"""
referenced = self.referenced_vertices()
inverse = np.zeros(len(self.vertices), dtype=settings.INT_DTYPE)
inverse[referenced] = np.arange(referenced.sum())
return self.update_vertices(
mask=referenced,
inverse=inverse,
)
[docs]
def update_elements(self, mask):
"""Similar to update_vertices, but for elements.
Parameters
-----------
mask: bool or (m,) np.ndarray
Returns
--------
new_self: type(self)
"""
self.elements = self.elements[mask]
return self.remove_unreferenced_vertices()
[docs]
def update_edges(self, *args, **kwargs):
"""Alias to update_elements."""
return self.update_elements(*args, **kwargs)
[docs]
def dashed(self, spacing=None):
"""Turn edges into dashed edges(=lines). Given spacing, it will try to
chop edges as close to it as possible. Pattern should look:
``dashed edges``
.. code-block::
o--------o o--------o o--------o
|<------>| |<-->|
(chop length) (chop length / 2)
Parameters
-----------
spacing: float
Default is None and it will use self.bounds_diagonal_norm() / 50
Returns
--------
dashing_edges: Edges
"""
if self.kind != "edge":
raise NotImplementedError("dashed is only for edges.")
if spacing is None:
# apply "automatic" spacing
spacing = self.bounds_diagonal_norm() / 50
v0s = self.vertices[self.edges[:, 0]]
v1s = self.vertices[self.edges[:, 1]]
distances = np.linalg.norm(v0s - v1s, axis=1)
linspaces = (((distances // (spacing * 1.5)) + 1) * 3).astype(np.int32)
# chop vertices!
new_vs = []
for v0, v1, lins in zip(v0s, v1s, linspaces):
new_vs.append(np.linspace(v0, v1, lins))
# we need all chopped vertices.
# there might be duplicating vertices. you can use merge_vertices
new_vs = np.vstack(new_vs)
# all mid points are explicitly defined, but they aren't required
# so, rm.
mask = np.ones(len(new_vs), dtype=bool)
mask[1::3] = False
new_vs = new_vs[mask]
# prepare edges
tmp_es = utils.connec.range_to_edges((0, len(new_vs)), closed=False)
new_es = tmp_es[::2]
return Edges(vertices=new_vs, edges=new_es)
[docs]
def shrink(self, ratio=0.8, map_vertex_data=True):
"""Returns shrunk elements.
Parameters
-----------
ratio: float
Default is 0.8
map_vertex_data: bool
Default is True. Maps all vertex_data.
Returns
--------
s_elements: Elements
shrunk elements
"""
elements = self.const_elements
vs = np.vstack(self.vertices[elements])
es = np.arange(len(vs))
nodes_per_element = elements.shape[1]
es = es.reshape(-1, nodes_per_element)
mids = np.repeat(self.centers(), nodes_per_element, axis=0)
vs -= mids
vs *= ratio
vs += mids
s_elements = type(self)(vertices=vs, elements=es)
if map_vertex_data:
elements_flat = elements.ravel()
for key, value in self.vertex_data.items():
s_elements.vertex_data[key] = value[elements_flat]
# probably wanna take visualization options too
s_elements._show_options._options = deepcopy(
self.show_options._options
)
return s_elements
[docs]
def to_vertices(self):
"""Returns Vertices obj.
Parameters
-----------
None
Returns
--------
vertices: Vertices
"""
return Vertices(self.vertices)
def _get_attr(self, attr):
"""Internal function to get attribute that maybe property or callable.
Some properties are replaced by callable in subclasses as it may depend
on other properties of subclass.
Parameters
-----------
attr: str
Returns
--------
attrib: Any
"""
attrib = getattr(self, attr)
return attrib() if callable(attrib) else attrib