"""Internal :vtk:`vtkAlgorithm` support helpers.""" from __future__ import annotations import traceback from typing import TYPE_CHECKING import numpy as np import pyvista from pyvista._deprecate_positional_args import _deprecate_positional_args from pyvista.core.errors import PyVistaPipelineError from pyvista.core.utilities.helpers import wrap from pyvista.core.utilities.misc import _NoNewAttrMixin from pyvista.plotting import _vtk if TYPE_CHECKING: from pyvista.core.utilities.arrays import CellLiteral from pyvista.core.utilities.arrays import PointLiteral def algorithm_to_mesh_handler( mesh_or_algo, port=0 ) -> tuple[pyvista.DataSet, _vtk.vtkAlgorithm | _vtk.vtkAlgorithmOutput | None]: """Handle :vtk:`vtkAlgorithms` where mesh objects are expected. This is a convenience method to handle :vtk:`vtkAlgorithms` when passed to methods that expect a :class:`~pyvista.DataSet`. This method will check if the passed object is a :vtk:`vtkAlgorithm` or :vtk:`vtkAlgorithmOutput` and if so, return that algorithm's output dataset (mesh) as the mesh to be used by the calling function. Parameters ---------- mesh_or_algo : DataSet | :vtk:`vtkAlgorithm` | :vtk:`vtkAlgorithmOutput` The input to be used as a data set (mesh) or :vtk:`vtkAlgorithm` object. port : int, default: 0 If the input (``mesh_or_algo``) is an algorithm, this specifies which output port to use on that algorithm for the returned mesh. Returns ------- mesh : pyvista.DataSet The resulting mesh data set from the input. algorithm : :vtk:`vtkAlgorithm` | :vtk:`vtkAlgorithmOutput` | None If an algorithm is passed, it will be returned. Otherwise returns ``None``. """ if isinstance(mesh_or_algo, (_vtk.vtkAlgorithm, _vtk.vtkAlgorithmOutput)): if isinstance(mesh_or_algo, _vtk.vtkAlgorithmOutput): algo = mesh_or_algo.GetProducer() # If vtkAlgorithmOutput, override port argument port = mesh_or_algo.GetIndex() output = mesh_or_algo else: algo = mesh_or_algo output = algo.GetOutputPort(port) algo.Update() # NOTE: this could be expensive... but we need it to get the mesh # for legacy implementation. This can be refactored. mesh = wrap(algo.GetOutputDataObject(port)) if mesh is None: # This is known to happen with vtkPointSet and VTKPythonAlgorithmBase # see workaround in PreserveTypeAlgorithmBase. # This check remains as a fail-safe. msg = 'The passed algorithm is failing to produce an output.' # type: ignore[unreachable] raise PyVistaPipelineError(msg) # NOTE: Return the vtkAlgorithmOutput only if port is non-zero. Segfaults can sometimes # happen with vtkAlgorithmOutput. This logic will mostly avoid those issues. # See https://gitlab.kitware.com/vtk/vtk/-/issues/18776 return mesh, output if port != 0 else algo return mesh_or_algo, None def set_algorithm_input(alg, inp, port=0): """Set the input to a :vtk:`vtkAlgorithm`. Parameters ---------- alg : :vtk:`vtkAlgorithm` The algorithm whose input is being set. inp : :vtk:`vtkAlgorithm` | :vtk:`vtkAlgorithmOutput` | :vtk:`vtkDataObject` The input to the algorithm. port : int, default: 0 The input port. """ if isinstance(inp, _vtk.vtkAlgorithm): alg.SetInputConnection(port, inp.GetOutputPort()) elif isinstance(inp, _vtk.vtkAlgorithmOutput): alg.SetInputConnection(port, inp) else: alg.SetInputDataObject(port, inp) class PreserveTypeAlgorithmBase( _NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.VTKPythonAlgorithmBase ): """Base algorithm to preserve type. Parameters ---------- nInputPorts : int, default: 1 Number of input ports for the algorithm. nOutputPorts : int, default: 1 Number of output ports for the algorithm. """ def __init__(self, nInputPorts=1, nOutputPorts=1): """Initialize algorithm.""" _vtk.VTKPythonAlgorithmBase.__init__( self, nInputPorts=nInputPorts, nOutputPorts=nOutputPorts, ) def GetInputData(self, inInfo, port, idx): """Get input data object. This will convert :vtk:`vtkPointSet` to :vtk:`vtkPolyData`. Parameters ---------- inInfo : :vtk:`vtkInformation` The information object associated with the input port. port : int The index of the input port. idx : int The index of the data object within the input port. Returns ------- :vtk:`vtkDataObject` The input data object. """ inp = wrap(_vtk.VTKPythonAlgorithmBase.GetInputData(self, inInfo, port, idx)) if isinstance(inp, pyvista.PointSet): return inp.cast_to_polydata() return inp # THIS IS CRUCIAL to preserve data type through filter def RequestDataObject(self, _request, inInfo, outInfo) -> int: """Preserve data type. Parameters ---------- _request : :vtk:`vtkInformation` The request object for the filter. inInfo : :vtk:`vtkInformationVector` The input information vector for the filter. outInfo : :vtk:`vtkInformationVector` The output information vector for the filter. Returns ------- int Returns 1 if successful. """ class_name = self.GetInputData(inInfo, 0, 0).GetClassName() if class_name == 'vtkPointSet': # See https://gitlab.kitware.com/vtk/vtk/-/issues/18771 self.OutputType = 'vtkPolyData' else: self.OutputType = class_name self.FillOutputPortInformation(0, outInfo.GetInformationObject(0)) return 1 class ActiveScalarsAlgorithm(PreserveTypeAlgorithmBase): """Algorithm to control active scalars. The output of this filter is a shallow copy of the input data set with the active scalars set as specified. Parameters ---------- name : str Name of scalars used to set as active on the output mesh. Accepts a string name of an array that is present on the mesh. Array should be sized as a single vector. preference : str, default: 'point' When ``mesh.n_points == mesh.n_cells`` and setting scalars, this parameter sets how the scalars will be mapped to the mesh. The default, ``'point'``, causes the scalars to be associated with the mesh points. Can be either ``'point'`` or ``'cell'``. """ def __init__(self, name: str, preference: PointLiteral | CellLiteral = 'point'): """Initialize algorithm.""" super().__init__() self.scalars_name = name self.preference = preference def RequestData(self, _request, inInfo, outInfo) -> int: """Perform algorithm execution. Parameters ---------- _request : :vtk:`vtkInformation` The request object. inInfo : :vtk:`vtkInformationVector` Information about the input data. outInfo : :vtk:`vtkInformationVector` Information about the output data. Returns ------- int 1 on success. """ try: inp = wrap(self.GetInputData(inInfo, 0, 0)) out = self.GetOutputData(outInfo, 0) output = inp.copy() if output.n_arrays: output.set_active_scalars(self.scalars_name, preference=self.preference) out.ShallowCopy(output) except Exception: # pragma: no cover traceback.print_exc() raise return 1 class PointSetToPolyDataAlgorithm( _NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.VTKPythonAlgorithmBase ): """Algorithm to cast PointSet to PolyData. This is implemented with :func:`pyvista.PointSet.cast_to_polydata`. """ def __init__(self): """Initialize algorithm.""" _vtk.VTKPythonAlgorithmBase.__init__( self, nInputPorts=1, nOutputPorts=1, inputType='vtkPointSet', outputType='vtkPolyData', ) def RequestData(self, _request, inInfo, outInfo) -> int: """Perform algorithm execution. Parameters ---------- _request : :vtk:`vtkInformation` Information associated with the request. inInfo : :vtk:`vtkInformationVector` Information about the input data. outInfo : :vtk:`vtkInformationVector` Information about the output data. Returns ------- int 1 when successful. """ try: inp = wrap(self.GetInputData(inInfo, 0, 0)) out = self.GetOutputData(outInfo, 0) output = inp.cast_to_polydata(deep=False) out.ShallowCopy(output) except Exception: # pragma: no cover traceback.print_exc() raise return 1 class AddIDsAlgorithm(PreserveTypeAlgorithmBase): """Algorithm to add point or cell IDs. Output of this filter is a shallow copy of the input with point and/or cell ID arrays added. Parameters ---------- point_ids : bool, default: True Whether to add point IDs. cell_ids : bool, default: True Whether to add cell IDs. Raises ------ ValueError If neither point IDs nor cell IDs are set. """ @_deprecate_positional_args def __init__(self, point_ids: bool = True, cell_ids: bool = True): # noqa: FBT001, FBT002 """Initialize algorithm.""" super().__init__() if not point_ids and not cell_ids: # pragma: no cover msg = 'IDs must be set for points or cells or both.' raise ValueError(msg) self.point_ids = point_ids self.cell_ids = cell_ids def RequestData(self, _request, inInfo, outInfo) -> int: """Perform algorithm execution. Parameters ---------- _request : :vtk:`vtkInformation` Information associated with the request. inInfo : :vtk:`vtkInformationVector` Information about the input data. outInfo : :vtk:`vtkInformationVector` Information about the output data. Returns ------- int Returns 1 if the algorithm was successful. Raises ------ Exception If the algorithm fails to execute properly. """ try: inp = wrap(self.GetInputData(inInfo, 0, 0)) out = self.GetOutputData(outInfo, 0) output = inp.copy() if self.point_ids: output.point_data['point_ids'] = np.arange(0, output.n_points, dtype=int) if self.cell_ids: output.cell_data['cell_ids'] = np.arange(0, output.n_cells, dtype=int) if output.active_scalars_name in ['point_ids', 'cell_ids']: output.active_scalars_name = inp.active_scalars_name out.ShallowCopy(output) except Exception: # pragma: no cover traceback.print_exc() raise return 1 class CrinkleAlgorithm(_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.VTKPythonAlgorithmBase): """Algorithm to crinkle cell IDs.""" def __init__(self): """Initialize algorithm.""" super().__init__( nInputPorts=2, outputType='vtkUnstructuredGrid', ) def RequestData(self, _request, inInfo, outInfo) -> int: """Perform algorithm execution based on the input data and produce the output. Parameters ---------- _request : :vtk:`vtkInformation` The request information associated with the algorithm. inInfo : :vtk:`vtkInformationVector` Information vector describing the input data. outInfo : :vtk:`vtkInformationVector` Information vector where the output data should be placed. Returns ------- int Status of the execution. Returns 1 on successful completion. """ try: clipped = wrap(self.GetInputData(inInfo, 0, 0)) source = wrap(self.GetInputData(inInfo, 1, 0)) out = self.GetOutputData(outInfo, 0) output = source.extract_cells(np.unique(clipped.cell_data['cell_ids'])) out.ShallowCopy(output) except Exception: # pragma: no cover traceback.print_exc() raise return 1 @_deprecate_positional_args(allowed=['inp']) def outline_algorithm(inp, generate_faces: bool = False): # noqa: FBT001, FBT002 """Add :vtk:`vtkOutlineFilter` to pipeline. Parameters ---------- inp : pyvista.Common Input data to be filtered. generate_faces : bool, default: False Whether to generate faces for the outline. Returns ------- :vtk:`vtkOutlineFilter` Outline filter applied to the input data. """ alg = _vtk.vtkOutlineFilter() set_algorithm_input(alg, inp) alg.SetGenerateFaces(generate_faces) return alg @_deprecate_positional_args(allowed=['inp']) def extract_surface_algorithm( # noqa: PLR0917 inp, pass_pointid: bool = False, # noqa: FBT001, FBT002 pass_cellid: bool = False, # noqa: FBT001, FBT002 nonlinear_subdivision=1, ): """Add :vtk:`vtkDataSetSurfaceFilter` to pipeline. Parameters ---------- inp : pyvista.Common Input data to be filtered. pass_pointid : bool, default: False If ``True``, pass point IDs to the output. pass_cellid : bool, default: False If ``True``, pass cell IDs to the output. nonlinear_subdivision : int, default: 1 Level of nonlinear subdivision. Returns ------- :vtk:`vtkDataSetSurfaceFilter` Surface filter applied to the input data. """ surf_filter = _vtk.vtkDataSetSurfaceFilter() surf_filter.SetPassThroughPointIds(pass_pointid) surf_filter.SetPassThroughCellIds(pass_cellid) if nonlinear_subdivision != 1: surf_filter.SetNonlinearSubdivisionLevel(nonlinear_subdivision) set_algorithm_input(surf_filter, inp) return surf_filter def active_scalars_algorithm(inp, name, preference='point'): """Add a filter that sets the active scalars. Parameters ---------- inp : pyvista.Common Input data to be filtered. name : str Name of the scalars to set as active. preference : str, default: 'point' Preference for the scalars to be set as active. Options are 'point', 'cell', or 'field'. Returns ------- :vtk:`vtkAlgorithm` Active scalars filter applied to the input data. """ alg = ActiveScalarsAlgorithm( name=name, preference=preference, ) set_algorithm_input(alg, inp) return alg def pointset_to_polydata_algorithm(inp): """Add a filter that casts PointSet to PolyData. Parameters ---------- inp : pyvista.PointSet Input point set to be cast to PolyData. Returns ------- :vtk:`vtkAlgorithm` Filter that casts the input PointSet to PolyData. """ alg = PointSetToPolyDataAlgorithm() set_algorithm_input(alg, inp) return alg @_deprecate_positional_args(allowed=['inp']) def add_ids_algorithm(inp, point_ids: bool = True, cell_ids: bool = True): # noqa: FBT001, FBT002 """Add a filter that adds point and/or cell IDs. Parameters ---------- inp : pyvista.DataSet The input data to which the IDs will be added. point_ids : bool, default: True If ``True``, point IDs will be added to the input data. cell_ids : bool, default: True If ``True``, cell IDs will be added to the input data. Returns ------- AddIDsAlgorithm AddIDsAlgorithm filter. """ alg = AddIDsAlgorithm(point_ids=point_ids, cell_ids=cell_ids) set_algorithm_input(alg, inp) return alg def crinkle_algorithm(clip, source): """Add a filter that crinkles a clip. Parameters ---------- clip : pyvista.DataSet The input data to be crinkled. source : pyvista.DataSet The source of the crinkle. Returns ------- CrinkleAlgorithm CrinkleAlgorithm filter. """ alg = CrinkleAlgorithm() set_algorithm_input(alg, clip, 0) set_algorithm_input(alg, source, 1) return alg @_deprecate_positional_args(allowed=['inp']) def cell_data_to_point_data_algorithm(inp, pass_cell_data: bool = False): # noqa: FBT001, FBT002 """Add a filter that converts cell data to point data. Parameters ---------- inp : pyvista.DataSet The input data whose cell data will be converted to point data. pass_cell_data : bool, default: False If ``True``, the original cell data will be passed to the output. Returns ------- :vtk:`vtkCellDataToPointData` The :vtk:`vtkCellDataToPointData` filter. """ alg = _vtk.vtkCellDataToPointData() alg.SetPassCellData(pass_cell_data) set_algorithm_input(alg, inp) return alg @_deprecate_positional_args(allowed=['inp']) def point_data_to_cell_data_algorithm(inp, pass_point_data: bool = False): # noqa: FBT001, FBT002 """Add a filter that converts point data to cell data. Parameters ---------- inp : pyvista.DataSet The input data whose point data will be converted to cell data. pass_point_data : bool, default: False If ``True``, the original point data will be passed to the output. Returns ------- :vtk:`vtkPointDataToCellData` :vtk:`vtkPointDataToCellData` algorithm. """ alg = _vtk.vtkPointDataToCellData() alg.SetPassPointData(pass_point_data) set_algorithm_input(alg, inp) return alg def triangulate_algorithm(inp): """Triangulate the input data. Parameters ---------- inp : :vtk:`vtkDataObject` The input data to be triangulated. Returns ------- :vtk:`vtkTriangleFilter` The triangle filter that has been applied to the input data. """ trifilter = _vtk.vtkTriangleFilter() trifilter.PassVertsOff() trifilter.PassLinesOff() set_algorithm_input(trifilter, inp) return trifilter def decimation_algorithm(inp, target_reduction): """Decimate the input data to the target reduction. Parameters ---------- inp : :vtk:`vtkDataObject` The input data to be decimated. target_reduction : float The target reduction amount, as a fraction of the original data. Returns ------- :vtk:`vtkQuadricDecimation` The decimation algorithm that has been applied to the input data. """ alg = _vtk.vtkQuadricDecimation() alg.SetTargetReduction(target_reduction) set_algorithm_input(alg, inp) return alg