import numpy as np
import homcloud.phtrees as phtrees
import homcloud.paraview_interface as pv_interface
import homcloud.plotly_3d as p3d
from homcloud.spatial_searcher import SpatialSearcher
[docs]
class PHTrees(object):
"""
This class represents PH trees computed from an alpha filtration.
Please see `Obayashi (2018) <https://doi.org/10.1137/17M1159439>`_ if you want to know
more about optimal volumes, and see `Obayashi (2023) <https://doi.org/10.1007/s41468-023-00119-8>`_
if you want to know more about stable volumes.
You can compute the PH trees by :meth:`PDList.from_alpha_filtration`
with ``save_boundary_map=True`` and ``save_phtrees=True`` arguments.
.. You can compute the PH trees by :meth:`PD.phtrees` from the diagram.
Example:
>>> import homcloud.interface as hc
>>> pointcloud = hc.example_data("tetrahedron")
>>> # Compute PDs and PHTrees
>>> pdlist = hc.PDList.from_alpha_filtration(pointcloud, save_boundary_map=True, save_phtrees=True)
>>> # Load phtrees
>>> phtrees = pdlist.dth_diagram(2).load_phtrees()
>>> # Query the node whose birth-death pair is nearest to (19, 21).
>>> node = phtrees.pair_node_nearest_to(19, 21)
>>> # Show birth time and death time
>>> node.birth_time()
19.600000000000005
>>> node.death_time()
21.069444444444443
>>> node.boundary_points()
[[0.0, 0.0, 0.0], [8.0, 0.0, 0.0], [5.0, 6.0, 0.0], [4.0, 2.0, 6.0]]
>>> node.boundary()
[[[0.0, 0.0, 0.0], [5.0, 6.0, 0.0], [4.0, 2.0, 6.0]],
[[0.0, 0.0, 0.0], [8.0, 0.0, 0.0], [5.0, 6.0, 0.0]],
[[8.0, 0.0, 0.0], [5.0, 6.0, 0.0], [4.0, 2.0, 6.0]],
[[0.0, 0.0, 0.0], [8.0, 0.0, 0.0], [4.0, 2.0, 6.0]]]
"""
def __init__(self, orig, spatial_searcher):
self.orig = orig
self.spatial_searcher = spatial_searcher
@staticmethod
def from_pdgm(pdgm):
return PHTrees(
phtrees.PHTrees.from_pdgm(pdgm, PHTrees.Node),
SpatialSearcher(pdgm.death_indices, pdgm.births, pdgm.deaths),
)
@property
def all_nodes(self):
"""
Return all nodes.
Returns:
list[:class:`PHTrees.Node`]: The nodes.
"""
return list(self.orig.nodes.values())
@property
def roots(self):
return self.orig.roots
[docs]
def nodes_of(self, pairs):
"""
Returns the nodes of trees corresponding to birth-death pairs
in `pairs`.
Args:
pairs (list[Pair]): The list of pairs.
Returns:
list[:class:`PHTrees.Node`]: The nodes.
"""
return [self._resolver().phtree.nodes[pair.death_index] for pair in pairs]
[docs]
def pair_node_nearest_to(self, x, y):
"""
Return the node corresponding the pair which is nearest to
(`x`, `y`).
Args:
x (float): The birth-axis coordinate.
y (float): The death-axis coordinate.
Returns:
:class:`PHTrees.Node`: The nearest node.
"""
return self.orig.nodes[self.spatial_searcher.nearest_pair(x, y)]
[docs]
def pair_nodes_in_rectangle(self, xmin, xmax, ymin, ymax):
"""
Returns the list of nodes corresponding to the birth-death
pairs in the given rectangle.
Args:
xmin (float): The minimum of the birth-axis of the rectangle.
xmax (float): The maximum of the birth-axis of the rectangle.
ymin (float): The minimum of the death-axis of the rectangle.
ymax (float): The maximum of the death-axis of the rectangle.
Returns:
list[:class:`PHTrees.Node`]: The nodes in the rectangle.
"""
return [
self.orig.nodes[death_index] for death_index in self.spatial_searcher.in_rectangle(xmin, xmax, ymin, ymax)
]
@staticmethod
def _to_paraview_node_from_nodes(all_vertices, nodes, gui_name=None):
f = pv_interface.TempFile(".vtk")
drawer = pv_interface.SimplexDrawer(len(nodes), all_vertices, {})
for i, node in enumerate(nodes):
node.draw_volume(drawer, i)
drawer.write(f)
f.close()
return pv_interface.VTK(f.name, gui_name, f).set_representation("Wireframe")
[docs]
def to_paraview_node_from_nodes(self, nodes, gui_name=None):
"""
Construct a :class:`homcloud.paraview_interface.PipelineNode`
object to visulize optimal volumes of the nodes.
Args:
nodes (list[:class:`PHTrees.Node`]): The list of nodes to be visualized.
gui_name (str | None): The name shown in Pipeline Browser
in paraview's GUI.
Returns:
homcloud.paraview_interface.VTK: Paraview Pipeline node object.
Notes:
All nodes should be nodes of `self` PHTrees.
"""
return self._to_paraview_node_from_nodes(self.orig.coord_resolver.vertices, nodes, gui_name)
to_pvnode_from_nodes = to_paraview_node_from_nodes
[docs]
class Volume(object):
"""
The superclass of :class:`PHTrees.Node` and :class:`PHTrees.StableVolume`.
Methods:
birth_time()
Returns:
float: The birth time of the corresponding birth-death pair.
death_time()
Returns:
float: The death time of the corresponding birth-death pair.
lifetime()
Returns:
float: The lifetime of the corresponding birth-death pair.
simplices()
Returns:
list[list[list[float]]], a.k.a list[Simplex]:
The simplices in the optimal volume.
boundary()
Returns:
list[list[float]], a.k.a. list[Point]:
Points in the volume optimal cycle.
birth_simplex()
Returns the birth simplex.
death_simplex()
Returns the death simplex.
ancestors()
Returns:
list[:class:`PHTrees.Node`]:
The ancestors of the tree node include itself.
"""
[docs]
def points(self):
"""
Returns:
list[list[float]], a.k.a list[Point]:
Points in the optimal volume.
"""
return self.vertices()
def volume(self):
return self.volume_nodes
[docs]
def boundary_points(self):
"""
Returns:
list[list[float]]: All vertices in the boundary of the optimal/stable volume
"""
return self.boundary_vertices()
[docs]
def points_symbols(self):
"""
Returns:
list[str]: All vertices in the optimal/stable volume
in the form of the symbolic representation.
"""
return self.vertices("symbols")
[docs]
def volume_simplices_symbols(self):
"""
Returns:
list[list[str]]: All simplices in optimal/stable volume
in the form of the symbolic representation.
"""
return self.simplices("symbols")
[docs]
def boundary_points_symbols(self):
"""
Returns:
list[str]: All vertices in the boundary of the optimal/stable volume
in the form of the symbolic representation.
"""
return self.boundary_vertices("symbols")
[docs]
def boundary_symbols(self):
"""
Returns:
list[list[str]]: All simplices in the optimal/stable volume
in the form of the symbolic representation.
"""
return self.boundary("symbols")
[docs]
def living(self):
"""
Returns:
bool: True if the birth time and death time of the node
are different.
"""
return self.birth_time() != self.death_time()
[docs]
def to_paraview_node(self, gui_name=None):
"""
Construct a :class:`homcloud.paraview_interface.PipelineNode`
object to visulize an optimal/stable volume of the node.
gui_name (str | None): The name shown in Pipeline Browser
in paraview's GUI.
Returns:
homcloud.paraview_interface.VTK: Paraview Pipeline node object.
"""
return PHTrees._to_paraview_node_from_nodes(self.trees.coord_resolver.vertices, [self], gui_name)
to_pvnode = to_paraview_node
[docs]
def to_plotly3d_trace(self, color="green", width=1, name=""):
"""
Constructs a plotly's trace object to visualize the optimal volume
Args:
color (str | None): The name of the color
width (int): The width of the line
name (str): The name of the object
Returns:
plotly.graph_objects.Scatter3d: Plotly's trace object
"""
return p3d.Simplices(self.boundary(), color, width, name)
to_plotly3d = to_plotly3d_trace
[docs]
def to_plotly3d_mesh(self, color="green", name=""):
"""
Constructs a plotly's trace object to visualize the face of an optimal/stable volume
Args:
color (str | None): The name of the color
name (str): The name of the object
Returns:
plotly.graph_objects.Mesh3d: Plotly's trace object
"""
return p3d.SimplicesMesh(self.boundary(), color, name)
[docs]
def to_pyvista_mesh(self):
"""
Constructs a PyVista's mesh object to visualize the face of an optimal/stable volume
Returns:
pyvista.PolyData: PyVista's mesh object
"""
import homcloud.pyvistahelper as pvhelper
return pvhelper.Triangles(self.boundary())
[docs]
class Node(Volume, phtrees.Node):
"""
The class represents a tree node of :class:`PHTrees`. A node have information about an optimal volume.
"""
def ancestors(self):
ret = [self]
while True:
parent = self.trees.parent_of(ret[-1])
if parent is None:
break
ret.append(parent)
return ret
[docs]
def living_descendants(self):
"""
Returns:
list[:class:`PHTrees.Node`]: All descendant nodes with positive lifetime
"""
return [node for node in self.volume_nodes if node.living()]
[docs]
def stable_volume(self, epsilon):
"""
Returns the stable volume corresponding to self.
Args:
epsilon (float): Duration noise strength
Returns:
:class:`PHTrees.StableVolume`: The stable volume
"""
return super().stable_volume(epsilon, PHTrees.StableVolume)
def stable_volume_information(self):
thresholds = np.array([child.birth_time() - self.birth_time() for child in self.children])
volumes = np.array([child.volume_size() for child in self.children])
return (thresholds, volumes)
def __repr__(self):
return "PHTrees.Node({}, {})".format(self.birth_time(), self.death_time())
[docs]
class StableVolume(Volume, phtrees.StableVolume):
"""
The class represents a stable volume in :class:`PHTrees`.
"""
pass