import operator
from homcloud.delegate import forwardable
import homcloud.optvol as optvol
import homcloud.paraview_interface as pv_interface
import homcloud.plotly_3d as p3d
[docs]
@forwardable
class Volume(object):
"""
This class represents a volume.
This is the superclass of OptimalVolume, StableVolume and
StableSubvolume.
Notes:
* point: list of float
* cell: simplex or cube, simplex is used if the filtration is
simplicial (alpha filtration) and cube is used if the filtration
is cubical.
* simplex: list of point
* cube: tuple[point, list of {0, 1}],
* ssimplex: list of string
"""
def __init__(self, pair, cell_indices, result=None):
self.pair = pair
self.diagram = pair.diagram
self.cell_indices = cell_indices
self.result = result
__delegator_definitions__ = {"diagram": ["get_geometry_resolver"]}
[docs]
def birth_time(self):
"""
Returns:
float: The birth time.
"""
return self.pair.birth_time()
[docs]
def death_time(self):
"""
Returns:
float: The death time.
"""
return self.pair.death_time()
[docs]
def lifetime(self):
"""
Returns:
float: The lifetime of the pair.
"""
return self.death_time() - self.birth_time()
[docs]
def death_position(self, by="default"):
"""
Args:
by (string): Format of return values, "default", "coordinates", "symbols", "vertex_indexes", or "vindexes"
Returns:
simplex: The death simplex.
"""
return self.get_geometry_resolver(by).resolve_cell(self.pair.death_index)
[docs]
def points(self, by="default"):
"""
Args:
by (string): Format of return values, "default", "coordinates", "symbols", "vertex_indexes", or "vindexes"
Returns:
list of point: All vertices in the optimal volume.
"""
return self.get_geometry_resolver(by).resolve_vertices(self.cell_indices)
[docs]
def points_symbols(self):
"""
Returns:
list of string: All vertices in the optimal volume
in the form of the symbolic representation.
Notes:
This method is the same as :meth:`points` ("symbols")`.
"""
return self.points("symbols")
[docs]
def boundary_points(self, by="default"):
"""
Args:
by (string): Format of return values, "default", "coordinates", "symbols", "vertex_indexes", or "vindexes"
Returns:
list of point: All vertices in the volume optimal cycle.
"""
return self.get_geometry_resolver(by).resolve_boundary_vertices(self.cell_indices)
[docs]
def boundary_points_symbols(self):
"""
Returns:
list of string: All vertices in the volume optimal cycle
in the form of the symbolic representation.
Notes:
This method is the same as :meth:`boundary_points` ("symbols").
"""
return self.boundary_points("symbols")
[docs]
def boundary(self, by="default", adjust_periodic_boundary=None):
"""
Args:
by (string): Format of return values, "default", "coordinates", "symbols", "vertex_indexes", or "vindexes"
adjust_periodic_boundary (Option[(float, float)]): periodic boundary treatment
Returns:
list of cells: All cells in the volume optimal cycle.
"""
return self.get_geometry_resolver(by, adjust_periodic_boundary).resolve_boundary(self.cell_indices)
[docs]
def boundary_symbols(self):
"""
Returns:
list of ssimplex: All simplices in the volume optimal cycle
in the form of the symbolic representation.
Notes:
This method is the same as :meth:`boundary` ("symbols")
"""
return self.boundary("symbols")
[docs]
def cells(self, by="default", adjust_periodic_boundary=None):
"""
Args:
by (string): Format of return values, "default", "coordinates", "symbols", "vertex_indexes", or "vindexes"
adjust_periodic_boundary (Option[(float, float)]): periodic boundary treatment
Returns:
list of cell: All cells in volume optimal cycles.
"""
return self.get_geometry_resolver(by, adjust_periodic_boundary).resolve_cells(self.cell_indices)
simplices = cells
[docs]
def simplices_symbols(self):
"""
Returns:
list of ssimplex: All simplices in volume optimal cycles
in the form of the symbolic representation.
Notes:
This method is the same as :meth:`simplices` ("symbols")
"""
return self.simplices("symbols")
volume_simplices_symbols = simplices_symbols
[docs]
def cubes(self, by="default"):
"""
Args:
by (string): Format of return values, "default", "coordinates", "symbols", "vertex_indexes", or "vindexes"
Returns:
list of cube: All cubes in volume optimal cycles.
"""
return self.cells(by)
[docs]
def children(self):
"""
Returns:
list of :class:`Pair`: All children pairs.
"""
from .pd import Pair
death_to_number = self.diagram.pd.death_index_to_pair_number
def valid(d):
return d != self.pair.death_index and d in death_to_number
return [Pair(self.diagram, death_to_number[d]) for d in self.cell_indices if valid(d)]
[docs]
def dump_to_dict(self):
"""Returns information about the optimal volume in the form of dict.
Users can reconstruct :class:`OptimalVolume` and :class:`StableVolume` from the dictionary
using :meth:`restore_from_dict`.
The method is useful to compute large number of optimal/stable volumes.
Returns:
dict: The information about the optimal volume.
"""
assert self.diagram.pdgm_id
return {
"type": type(self).__name__,
"pdgm_id": self.diagram.pdgm_id,
"degree": self.diagram.degree,
"nth": self.pair.nth,
"birth_time": self.pair.birth_time(),
"death_time": self.pair.death_time(),
"cell_indices": self.cell_indices,
}
@classmethod
def validate_restore_data(cls, pd, data):
assert pd.pdgm_id == data["pdgm_id"]
assert cls.__name__ == data["type"]
assert pd.degree == data["degree"]
[docs]
@classmethod
def restore_from_dict(cls, pd, data, validate=True):
"""Returns :class:`Volume` object reconstructed from pd and data.
The data should be a dictionary returned by :meth:`dump_to_dict`.
This class method should be called with a subclass of Volume,
:class:`OptimalVolume`, :class:`StableVolume`, or :class:`StableSubvolume`, not Volume class.
Args:
pd (:class:`PD`): Persistence diagram object related to the volume
data (dict): A dictionary which contains volume information
validate (bool): Validate the information in the dictionary if True
Returns:
Volume: restored volume object
"""
if validate:
cls.validate_restore_data(pd, data)
return cls(pd.pair(data["nth"]), data["cell_indices"])
#: The alias of :meth:`death_position`.
death_pos = death_position
[docs]
def boundary_loop(self, by="default", adjust_periodic_boundary=None):
"""
Args:
by (string): Format of return values, "default", "coordinates", "symbols", "vertex_indexes", or "vindexes"
adjust_periodic_boundary (Option[(float, float)]): periodic boundary treatment
Returns:
Optional[list of point]: The list of points in the loop order.
Return None if the boundary consists of multiple loops.
Raises:
ValueError: Raised if the dimension of the volume is not 2D,
or if the loop is a self-loop or multi-edge loop.
"""
return self.get_geometry_resolver(by, adjust_periodic_boundary).resolve_boundary_loop(self.cell_indices)
[docs]
def boundary_loop_symbols(self):
"""
Returns:
Optional[List[str]]: The list of vertex symbols in the loop order.
Return None if the boundary consists of multiple loops.
Raises:
ValueError: Raised if the dimension of the volume is not 2D,
or if the loop is a self-loop or multi-edge loop.
"""
return self.boundary_loop("symbols")
[docs]
def to_paraview_node(self, gui_name=None):
"""
Construct a :class:`homcloud.paraview_interface.PipelineNode` object
to visulize an optimal volume.
You can show the optimal volume by
:meth:`homcloud.paraview_interface.show`. You can also
adjust the visual by the methods of
:class:`homcloud.paraview_interface.PipelineNode`.
Args:
gui_name (string or None): The name shown in Pipeline Browser
in paraview's GUI.
Returns:
homcloud.paraview_interface.PipelineNode: A PipelineNode object.
"""
return OptimalVolume.to_paraview_node_for_volumes([self], gui_name)
to_pvnode = to_paraview_node
[docs]
@staticmethod
def to_paraview_node_for_volumes(volumes, gui_name=None):
"""
Construct a :class:`homcloud.paraview_interface.PipelineNode` object
to visulize multiple optimal volumes.
All optimal volumes should come from the same :class:`PD` object.
Args:
volumes (list of :class:`OptimalVolume`): The optimal volumes to be
visualized.
gui_name (string or None): The name shown in Pipeline Browser
in paraview's GUI.
Returns:
homcloud.paraview_interface.PipelineNode: A PipelineNode object.
"""
diagram = volumes[0].diagram
if diagram.filtration_type == "alpha":
geom_resolver = diagram.pd.alpha_coord_resolver
drawer = optvol.drawer_for_alpha(geom_resolver.vertices)
elif diagram.filtration_type == "cubical":
geom_resolver = diagram.pd.cubical_geometry_resolver
drawer = optvol.drawer_for_cubical(geom_resolver.shape)
optvol.draw_volumes(drawer, map(operator.attrgetter("result"), volumes), geom_resolver)
f = pv_interface.TempFile(".vtk")
drawer.write(f)
f.close()
return pv_interface.VTK(f.name, gui_name, f).set_representation("Wireframe")
[docs]
def to_plotly3d_trace(self, color="green", width=1, name=""):
"""
Constructs a plotly's trace object to visualize the optimal volume
Args:
color (string or None): The name of the color
width (int): The width of the line
name (string): The name of the object
Returns:
plotly.graph_objects.Scatter3d: Plotly's trace object
"""
if self.diagram.filtration_type == "alpha":
return p3d.Simplices(self.boundary(), color, width, name)
elif self.diagram.filtration_type == "cubical":
return p3d.Cubes(self.boundary(), color, width, name)
else:
raise RuntimeError(f"{self.filtration_type} cannot be renderred")
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 volume
Args:
color (string or None): The name of the color
name (string): The name of the object
Returns:
plotly.graph_objects.Mesh3d: Plotly's trace object
"""
if self.diagram.filtration_type == "alpha":
if self.diagram.degree == 2:
return p3d.SimplicesMesh(self.boundary(), color, name)
elif self.diagram.degree == 1:
return p3d.SimplicesMesh(self.simplices(), color, name)
else:
raise RuntimeError(f"dim {self.diagram.degree} volume is available for plotly")
elif self.diagram.filtration_type == "cubical":
if self.diagram.degree == 2:
return p3d.CubesMesh(self.boundary(), color, name)
else:
raise RuntimeError(f"{self.filtration_type} cannot be renderred")
[docs]
def to_pyvista_boundary_mesh(self, adjust_periodic_boundary=None):
"""
Constructs a PyVista's mesh object to visualize the boundary of an optimal/stable volume.
Args:
adjust_periodic_boundary (Option[(float, float)]): periodic boundary treatment
Returns:
pyvista.PolyData: PyVista's mesh object
"""
import homcloud.pyvistahelper as pvhelper
if self.diagram.filtration_type == "alpha":
if self.diagram.degree == 2:
return pvhelper.Triangles(self.boundary("coordinates", adjust_periodic_boundary))
elif self.diagram.degree == 1:
return pvhelper.Lines(self.boundary("coordinates", adjust_periodic_boundary))
else:
raise RuntimeError(f"dim {self.diagram.degree} volume is available for pyvista")
raise RuntimeError(f"{self.filtration_type} cannot be renderred")
[docs]
def to_pyvista_volume_mesh(self, adjust_periodic_boundary=None):
"""
Constructs a PyVista's mesh object to visualize the internal face of a 1D optimal/stable volume.
Args:
adjust_periodic_boundary (Option[(float, float)]): periodic boundary treatment
Returns:
pyvista.PolyData: PyVista's mesh object
"""
import homcloud.pyvistahelper as pvhelper
if self.diagram.filtration_type == "alpha":
if self.diagram.degree == 1:
return pvhelper.Triangles(self.cells("coordinates", adjust_periodic_boundary))
raise RuntimeError("Volume mesh can only be applied to 1d alpha volume")
[docs]
class OptimalVolume(Volume):
"""
This class represents an optimal volume.
"""
[docs]
def birth_position(self, by="default"):
"""
Args:
by (string): Format of return values, "default", "coordinates", "symbols", "vertex_indexes", or "vindexes"
Returns:
simplex: The birth simplex.
"""
return self.get_geometry_resolver(by).resolve_cell(self.pair.birth_index)
#: The alias of :meth:`birth_position`.
birth_pos = birth_position
[docs]
def stable_subvolume(self, threshold, solver=None, solver_options=[]):
"""
Returns the stable subvolume of the optimal volume.
Args:
threshold (float): The noise bandwidth.
Returns:
StableSubvolume: The stable subvolume.
"""
lp_solver = optvol.find_lp_solver(solver, solver_options)
ssvfinder = optvol.TightenedSubVolumeFinder(
self.diagram.optvol_optimizer_builder(None, None, lp_solver), self.diagram.pd.index_to_level, threshold
)
result = ssvfinder.find(self.pair.birth_index, self.pair.death_index, self.cell_indices)
return StableSubvolume(self.pair, result.cell_indices, threshold, result)
tightened_subvolume = stable_subvolume
[docs]
class EigenVolume(Volume):
"""
This class represents an "eigenvolume". It is the superclass of
StableVolume and StableSubvolume.
Attributes:
threshold (float): The threshold used for the computation of the
eigenvolume.
"""
def __init__(self, pair, cell_indices, threshold, result=None):
super().__init__(pair, cell_indices, result)
self.threshold = threshold
[docs]
def dump_to_dict(self):
data = super().dump_to_dict()
data["threshold"] = self.threshold
return data
[docs]
@classmethod
def restore_from_dict(cls, pd, data, validate=True):
if validate:
cls.validate_restore_data(pd, data)
return cls(pd.pair(data["nth"]), data["cell_indices"], data["threshold"])
[docs]
class StableVolume(EigenVolume):
"""
This class represents a stable volume.
The instance is given by :meth:`Pair.stable_volume`.
"""
pass
[docs]
class StableSubvolume(EigenVolume):
"""
This class represents a stable subvolume.
The instance is given by :meth:`OptimalVolume.stable_subvolume`.
"""
pass