Repository: pyvista/pyvista

Issue:
Cell wise and dimension reducing filters do not work for PointSet
### Describe the bug, what's wrong, and what you expected.

`PointSet` is an odd data type as it has no cells and simply represents a 0-dimensional geometry: point clouds.

This means that two common types of operations are not possible on this data type:

1. Cell-wise operations like thresholding
2. Dimension-reducing operations like contouring

Cell wise operations can easily be fixed by adding cells. This can be done with `cast_to_polydata()`

Dimension-reducing operations, on the other hand, have no solution and should not be allowed on `PointSet`.

How can we properly error out or convert to `PolyData` when calling dataset filters like `threshold()` and `contour()`? Should these types of filters be overridden on the `PointSet` class?

### Steps to reproduce the bug.

### Cell wise operation produces *invalid* output

```python
import pyvista as pv
import numpy as np
pc = pv.PointSet(np.random.random((100, 3)))
pc['foo'] = np.arange(pc.n_points)
pc.threshold()
```

<img width="575" alt="Screen Shot 2023-01-11 at 5 47 02 PM 1" src="https://user-images.githubusercontent.com/22067021/211949301-8d10e9ac-172e-4f27-ad81-c3ec2d335263.png">


### Dimension reducing operation produces *no* output

```py
import pyvista as pv
import numpy as np
pc = pv.PointSet(np.random.random((100, 3)))
pc['foo'] = np.arange(pc.n_points)
pc.contour()
```
<img width="417" alt="Screen Shot 2023-01-11 at 5 47 57 PM" src="https://user-images.githubusercontent.com/22067021/211949430-a3e77292-6b1e-4d2d-b2e3-b2a640ed65fc.png">



### System Information

```shell
n/a
```


### Screenshots

n/a


Discussion:

Patch:
diff --git a/doc/source/conf.py b/doc/source/conf.py
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -209,6 +209,8 @@
     # Type alias
     r'\.ColorLike$',
     r'\.Chart$',
+    # PointSet *args and **kwargs for wrapped parameters
+    r'\.PointSet(\.|$)',
     # Mixin methods from collections.abc
     r'\.MultiBlock\.clear$',
     r'\.MultiBlock\.count$',
diff --git a/pyvista/core/errors.py b/pyvista/core/errors.py
--- a/pyvista/core/errors.py
+++ b/pyvista/core/errors.py
@@ -46,3 +46,29 @@ def __init__(
     ):
         """Empty init."""
         RuntimeError.__init__(self, message)
+
+
+class PointSetNotSupported(TypeError):
+    """Requested filter or property is not supported by the PointSet class."""
+
+    def __init__(self, message='The requested operation is not supported for PointSets.'):
+        """Empty init."""
+        TypeError.__init__(self, message)
+
+
+class PointSetCellOperationError(PointSetNotSupported):
+    """Requested filter or property is not supported by the PointSet class."""
+
+    def __init__(self, message='Cell operations are not supported. PointSets contain no cells.'):
+        """Empty init."""
+        PointSetNotSupported.__init__(self, message)
+
+
+class PointSetDimensionReductionError(PointSetNotSupported):
+    """Requested filter or property is not supported by the PointSet class."""
+
+    def __init__(
+        self, message='Slice and other dimension reducing filters are not supported on PointSets.'
+    ):
+        """Empty init."""
+        PointSetNotSupported.__init__(self, message)
diff --git a/pyvista/core/pointset.py b/pyvista/core/pointset.py
--- a/pyvista/core/pointset.py
+++ b/pyvista/core/pointset.py
@@ -19,7 +19,13 @@
 from ..utilities.fileio import get_ext
 from .celltype import CellType
 from .dataset import DataSet
-from .errors import DeprecationError, VTKVersionError
+from .errors import (
+    DeprecationError,
+    PointSetCellOperationError,
+    PointSetDimensionReductionError,
+    PointSetNotSupported,
+    VTKVersionError,
+)
 from .filters import PolyDataFilters, StructuredGridFilters, UnstructuredGridFilters, _get_output
 
 DEFAULT_INPLACE_WARNING = (
@@ -323,6 +329,112 @@ def plot(self, *args, **kwargs):
         kwargs.setdefault('style', 'points')
         return pdata.plot(*args, **kwargs)
 
+    @wraps(PolyDataFilters.threshold)
+    def threshold(self, *args, **kwargs):
+        """Cast to PolyData and threshold.
+
+        Need this because cell-wise operations fail for PointSets.
+        """
+        return self.cast_to_polydata(False).threshold(*args, **kwargs).cast_to_pointset()
+
+    @wraps(PolyDataFilters.threshold_percent)
+    def threshold_percent(self, *args, **kwargs):
+        """Cast to PolyData and threshold.
+
+        Need this because cell-wise operations fail for PointSets.
+        """
+        return self.cast_to_polydata(False).threshold_percent(*args, **kwargs).cast_to_pointset()
+
+    @wraps(PolyDataFilters.explode)
+    def explode(self, *args, **kwargs):
+        """Cast to PolyData and explode.
+
+        The explode filter relies on cells.
+
+        """
+        return self.cast_to_polydata(False).explode(*args, **kwargs).cast_to_pointset()
+
+    @wraps(PolyDataFilters.delaunay_3d)
+    def delaunay_3d(self, *args, **kwargs):
+        """Cast to PolyData and run delaunay_3d."""
+        return self.cast_to_polydata(False).delaunay_3d(*args, **kwargs)
+
+    @property
+    def area(self) -> float:
+        """Return 0.0 since a PointSet has no area."""
+        return 0.0
+
+    @property
+    def volume(self) -> float:
+        """Return 0.0 since a PointSet has no volume."""
+        return 0.0
+
+    def contour(self, *args, **kwargs):
+        """Raise dimension reducing operations are not supported."""
+        raise PointSetNotSupported(
+            'Contour and other dimension reducing filters are not supported on PointSets'
+        )
+
+    def cell_data_to_point_data(self, *args, **kwargs):
+        """Raise PointSets do not have cells."""
+        raise PointSetNotSupported('PointSets contain no cells or cell data.')
+
+    def point_data_to_cell_data(self, *args, **kwargs):
+        """Raise PointSets do not have cells."""
+        raise PointSetNotSupported('PointSets contain no cells or cell data.')
+
+    def triangulate(self, *args, **kwargs):
+        """Raise cell operations are not supported."""
+        raise PointSetCellOperationError
+
+    def decimate_boundary(self, *args, **kwargs):
+        """Raise cell operations are not supported."""
+        raise PointSetCellOperationError
+
+    def find_cells_along_line(self, *args, **kwargs):
+        """Raise cell operations are not supported."""
+        raise PointSetCellOperationError
+
+    def tessellate(self, *args, **kwargs):
+        """Raise cell operations are not supported."""
+        raise PointSetCellOperationError
+
+    def slice(self, *args, **kwargs):
+        """Raise dimension reducing operations are not supported."""
+        raise PointSetDimensionReductionError
+
+    def slice_along_axis(self, *args, **kwargs):
+        """Raise dimension reducing operations are not supported."""
+        raise PointSetDimensionReductionError
+
+    def slice_along_line(self, *args, **kwargs):
+        """Raise dimension reducing operations are not supported."""
+        raise PointSetDimensionReductionError
+
+    def slice_implicit(self, *args, **kwargs):
+        """Raise dimension reducing operations are not supported."""
+        raise PointSetDimensionReductionError
+
+    def slice_orthogonal(self, *args, **kwargs):
+        """Raise dimension reducing operations are not supported."""
+        raise PointSetDimensionReductionError
+
+    def shrink(self, *args, **kwargs):
+        """Raise cell operations are not supported."""
+        raise PointSetCellOperationError
+
+    def separate_cells(self, *args, **kwargs):
+        """Raise cell operations are not supported."""
+        raise PointSetCellOperationError
+
+    def remove_cells(self, *args, **kwargs):
+        """Raise cell operations are not supported."""
+        raise PointSetCellOperationError
+
+    def point_is_inside_cell(self, *args, **kwargs):
+        """Raise cell operations are not supported."""
+        raise PointSetCellOperationError
+
 
 class PolyData(_vtk.vtkPolyData, _PointSet, PolyDataFilters):
     """Dataset consisting of surface geometry (e.g. vertices, lines, and polygons).


Test patch:
diff --git a/tests/test_pointset.py b/tests/test_pointset.py
--- a/tests/test_pointset.py
+++ b/tests/test_pointset.py
@@ -5,6 +5,11 @@
 import vtk
 
 import pyvista
+from pyvista.core.errors import (
+    PointSetCellOperationError,
+    PointSetDimensionReductionError,
+    PointSetNotSupported,
+)
 
 # skip all tests if concrete pointset unavailable
 pytestmark = pytest.mark.skipif(
@@ -19,6 +24,8 @@ def test_pointset_basic():
     assert pset.n_cells == 0
     assert 'PointSet' in str(pset)
     assert 'PointSet' in repr(pset)
+    assert pset.area == 0
+    assert pset.volume == 0
 
 
 def test_pointset_from_vtk():
@@ -210,6 +217,84 @@ def test_flip_normal():
     )
 
 
+def test_threshold(pointset):
+    pointset['scalars'] = range(pointset.n_points)
+    out = pointset.threshold(pointset.n_points // 2)
+    assert isinstance(out, pyvista.PointSet)
+    assert out.n_points == pointset.n_points // 2
+
+
+def test_threshold_percent(pointset):
+    pointset['scalars'] = range(pointset.n_points)
+    out = pointset.threshold_percent(50)
+    assert isinstance(out, pyvista.PointSet)
+    assert out.n_points == pointset.n_points // 2
+
+
+def test_explode(pointset):
+    out = pointset.explode(1)
+    assert isinstance(out, pyvista.PointSet)
+    ori_xlen = pointset.bounds[1] - pointset.bounds[0]
+    new_xlen = out.bounds[1] - out.bounds[0]
+    assert np.isclose(2 * ori_xlen, new_xlen)
+
+
+def test_delaunay_3d(pointset):
+    out = pointset.delaunay_3d()
+    assert isinstance(out, pyvista.UnstructuredGrid)
+    assert out.n_cells > 10
+
+
+def test_raise_unsupported(pointset):
+    with pytest.raises(PointSetNotSupported):
+        pointset.contour()
+
+    with pytest.raises(PointSetNotSupported):
+        pointset.cell_data_to_point_data()
+
+    with pytest.raises(PointSetNotSupported):
+        pointset.point_data_to_cell_data()
+
+    with pytest.raises(PointSetCellOperationError):
+        pointset.triangulate()
+
+    with pytest.raises(PointSetCellOperationError):
+        pointset.decimate_boundary()
+
+    with pytest.raises(PointSetCellOperationError):
+        pointset.find_cells_along_line()
+
+    with pytest.raises(PointSetCellOperationError):
+        pointset.tessellate()
+
+    with pytest.raises(PointSetDimensionReductionError):
+        pointset.slice()
+
+    with pytest.raises(PointSetDimensionReductionError):
+        pointset.slice_along_axis()
+
+    with pytest.raises(PointSetDimensionReductionError):
+        pointset.slice_along_line()
+
+    with pytest.raises(PointSetDimensionReductionError):
+        pointset.slice_implicit()
+
+    with pytest.raises(PointSetDimensionReductionError):
+        pointset.slice_orthogonal()
+
+    with pytest.raises(PointSetCellOperationError):
+        pointset.shrink()
+
+    with pytest.raises(PointSetCellOperationError):
+        pointset.separate_cells()
+
+    with pytest.raises(PointSetCellOperationError):
+        pointset.remove_cells()
+
+    with pytest.raises(PointSetCellOperationError):
+        pointset.point_is_inside_cell()
+
+
 def test_rotate_x():
     np_points = np.array([1, 1, 1], dtype=float)
     pset = pyvista.PointSet(np_points)