Repository: pyvista/pyvista

Issue:
Clean up and clarify sampling-like filters
### Describe what maintenance you would like added.

There was a discussion on slack on the use of sampling-like filters, i.e. `sample`, `probe`, and `interpolate`.  One issue is that it is hard to figure out when to use which filter.  The other issue is that `probe` has the opposite behavior of `sample` and `interpolate` in regards to order of operation (see below).

### Links to source code.

_No response_

### Pseudocode or Screenshots

```python
import pyvista as pv

small = pv.ImageData(dimensions=(5, 5, 5))
large = pv.ImageData(dimensions=(10, 10, 10))
print(small.n_points)
print(large.n_points)
print(small.probe(large).n_points)  # gives different result
print(small.sample(large).n_points)
print(small.interpolate(large).n_points)
```


This  gives

```txt
125
1000
1000
125
125
```


Discussion:
`probe` uses [vtkProbeFilter](https://vtk.org/doc/nightly/html/classvtkProbeFilter.html).  Note that `vtkCompositeDataProbeFilter` is a subclass, and adds the ability to use Composite Data as in input (only one of the two slots in the algorithm).

`sample` uses [vtkResampleWithDataSet](https://vtk.org/doc/nightly/html/classvtkResampleWithDataSet.html#details).  This uses `vtkCompositeDataProbeFilter` under the hood, but also allows composite data to be used in both the source and the input.

So I propose that we deprecate `probe` and only keep `sample` and `interpolate`.

`imterpolate` is separate from the other two since it uses a different sampling/interpolation method. 
The pyvista standard, at least in my experience, is that we should generally expect the shape of the mesh to be equal to the mesh on which the filter attribute is called.  That is, `mesh1.filter(mesh2)` should return a mesh closer to `mesh1`.  This also enables `inplace=True` usage when possible.

So, if we were to keep `probe` we should switch the order of operation, and the deprecation/breaking change would have to be done carefully.  I still think it should be removed entirely instead as above, but wanted to lay out other options.
> So, if we were to keep `probe` we should switch the order of operation, and the deprecation/breaking change would have to be done carefully. I still think it should be removed entirely instead as above, but wanted to lay out other options.

From a design standpoint, we'd probably have to do the deprecation the same way: deprecating the old method and introducing a new one that has the right semantics. Switching the input and output is not the kind of change we should subject downstream to.
+1 to this, I've definitely been confused by it before. 

> The pyvista standard, at least in my experience, is that we should generally expect the shape of the mesh to be equal to the mesh on which the filter attribute is called. 

+1 again

> From a design standpoint, we'd probably have to do the deprecation the same way: deprecating the old method and introducing a new one that has the right semantics. Switching the input and output is not the kind of change we should subject downstream to.

deprecate `probe` and introduce `eborp` 😉 

+1 for `eborp`

Patch:
diff --git a/examples/01-filter/interpolate.py b/examples/01-filter/interpolate.py
--- a/examples/01-filter/interpolate.py
+++ b/examples/01-filter/interpolate.py
@@ -4,6 +4,22 @@
 Interpolating
 ~~~~~~~~~~~~~
 
+There are two main methods of interpolating or sampling data from a target mesh
+in PyVista. :func:`pyvista.DataSetFilters.interpolate` uses a distance weighting
+kernel to interpolate point data from nearby points of the target mesh onto
+the desired points.
+:func:`pyvista.DataSetFilters.sample` interpolates data using the
+interpolation scheme of the enclosing cell from the target mesh.
+
+If the target mesh is a point cloud, i.e. there is no connectivity in the cell
+structure, then :func:`pyvista.DataSetFilters.interpolate` is typically
+preferred.  If interpolation is desired within the cells of the target mesh, then
+:func:`pyvista.DataSetFilters.sample` is typically desired.
+
+
+This example uses :func:`pyvista.DataSetFilters.interpolate`.
+For :func:`pyvista.DataSetFilters.sample`, see :ref:`resampling_example`.
+
 Interpolate one mesh's point/cell arrays onto another mesh's nodes using a
 Gaussian Kernel.
 """
diff --git a/examples/01-filter/resample.py b/examples/01-filter/resample.py
--- a/examples/01-filter/resample.py
+++ b/examples/01-filter/resample.py
@@ -4,10 +4,27 @@
 Resampling
 ~~~~~~~~~~
 
+There are two main methods of interpolating or sampling data from a target mesh
+in PyVista. :func:`pyvista.DataSetFilters.interpolate` uses a distance weighting
+kernel to interpolate point data from nearby points of the target mesh onto
+the desired points.
+:func:`pyvista.DataSetFilters.sample` interpolates data using the
+interpolation scheme of the enclosing cell from the target mesh.
+
+If the target mesh is a point cloud, i.e. there is no connectivity in the cell
+structure, then :func:`pyvista.DataSetFilters.interpolate` is typically
+preferred.  If interpolation is desired within the cells of the target mesh, then
+:func:`pyvista.DataSetFilters.sample` is typically desired.
+
+
+This example uses :func:`pyvista.DataSetFilters.sample`.
+For :func:`pyvista.DataSetFilters.interpolate`, see :ref:`interpolate_example`.
+
+
 Resample one mesh's point/cell arrays onto another mesh's nodes.
 """
 ###############################################################################
-# This example will resample a volumetric mesh's  scalar data onto the surface
+# This example will resample a volumetric mesh's scalar data onto the surface
 # of a sphere contained in that volume.
 
 # sphinx_gallery_thumbnail_number = 3
@@ -17,7 +34,7 @@
 ###############################################################################
 # Simple Resample
 # +++++++++++++++
-# Query a grids points onto a sphere
+# Query a grid's points onto a sphere
 mesh = pv.Sphere(center=(4.5, 4.5, 4.5), radius=4.5)
 data_to_probe = examples.load_uniform()
 
diff --git a/pyvista/core/filters/data_set.py b/pyvista/core/filters/data_set.py
--- a/pyvista/core/filters/data_set.py
+++ b/pyvista/core/filters/data_set.py
@@ -8,7 +8,12 @@
 
 import pyvista
 import pyvista.core._vtk_core as _vtk
-from pyvista.core.errors import AmbiguousDataError, MissingDataError, VTKVersionError
+from pyvista.core.errors import (
+    AmbiguousDataError,
+    MissingDataError,
+    PyVistaDeprecationWarning,
+    VTKVersionError,
+)
 from pyvista.core.filters import _get_output, _update_alg
 from pyvista.core.utilities import transformations
 from pyvista.core.utilities.arrays import (
@@ -3024,7 +3029,12 @@ def probe(
     ):
         """Sample data values at specified point locations.
 
-        This uses :class:`vtk.vtkProbeFilter`.
+        .. deprecated:: 0.41.0
+          `probe` will be removed in a future version. Use
+          :func:`pyvista.DataSetFilters.sample` instead.
+          If using `mesh1.probe(mesh2)`, use `mesh2.sample(mesh1)`.
+
+        This uses :class:`vtkProbeFilter`.
 
         Parameters
         ----------
@@ -3068,11 +3078,21 @@ def probe(
         >>> from pyvista import examples
         >>> mesh = pv.Sphere(center=(4.5, 4.5, 4.5), radius=4.5)
         >>> grid = examples.load_uniform()
-        >>> result = grid.probe(mesh)
-        >>> 'Spatial Point Data' in result.point_data
+        >>> result = grid.probe(mesh)  # doctest:+SKIP
+        >>> 'Spatial Point Data' in result.point_data  # doctest:+SKIP
         True
 
         """
+        # deprecated in v0.41.0
+        # remove in v0.44.0
+        warnings.warn(
+            """probe filter is deprecated and will be removed in a future version.
+            Use sample filter instead.
+            If using `mesh1.probe(mesh2)`, use `mesh2.sample(mesh1)`.
+            """,
+            PyVistaDeprecationWarning,
+        )
+
         if not pyvista.is_pyvista_dataset(points):
             points = wrap(points)
         alg = _vtk.vtkProbeFilter()
@@ -3100,9 +3120,17 @@ def sample(
         pass_point_data=True,
         categorical=False,
         progress_bar=False,
+        locator=None,
     ):
         """Resample array data from a passed mesh onto this mesh.
 
+        For `mesh1.sample(mesh2)`, the arrays from `mesh2` are sampled onto
+        the points of `mesh1`.  This function interpolates within an
+        enclosing cell.  This contrasts with
+        :function`pyvista.DataSetFilters.interpolate` that uses a distance
+        weighting for nearby points.  If there is cell topology, `sample` is
+        usually preferred.
+
         This uses :class:`vtk.vtkResampleWithDataSet`.
 
         Parameters
@@ -3130,22 +3158,41 @@ def sample(
         progress_bar : bool, default: False
             Display a progress bar to indicate progress.
 
+        locator : vtkAbstractCellLocator, optional
+            Prototype cell locator to perform the ``FindCell()``
+            operation.  Default uses the DataSet ``FindCell`` method.
+
         Returns
         -------
         pyvista.DataSet
             Dataset containing resampled data.
 
+        See Also
+        --------
+        pyvista.DataSetFilters.interpolate
+
         Examples
         --------
         Resample data from another dataset onto a sphere.
 
-        >>> import pyvista
+        >>> import pyvista as pv
         >>> from pyvista import examples
-        >>> mesh = pyvista.Sphere(center=(4.5, 4.5, 4.5), radius=4.5)
+        >>> mesh = pv.Sphere(center=(4.5, 4.5, 4.5), radius=4.5)
         >>> data_to_probe = examples.load_uniform()
         >>> result = mesh.sample(data_to_probe)
         >>> result.plot(scalars="Spatial Point Data")
 
+        If sampling from a set of points represented by a ``(n, 3)``
+        shaped ``numpy.ndarray``, they need to be converted to a
+        PyVista DataSet, e.g. :class:`pyvista.PolyData`, first.
+
+        >>> import numpy as np
+        >>> points = np.array([[1.5, 5.0, 6.2], [6.7, 4.2, 8.0]])
+        >>> mesh = pv.PolyData(points)
+        >>> result = mesh.sample(data_to_probe)
+        >>> result["Spatial Point Data"]
+        pyvista_ndarray([ 46.5 , 225.12])
+
         See :ref:`resampling_example` for more examples using this filter.
 
         """
@@ -3161,6 +3208,9 @@ def sample(
         if tolerance is not None:
             alg.SetComputeTolerance(False)
             alg.SetTolerance(tolerance)
+        if locator:
+            alg.SetCellLocatorPrototype(locator)
+
         _update_alg(alg, progress_bar, 'Resampling array Data from a Passed Mesh onto Mesh')
         return _get_output(alg)
 
@@ -3178,8 +3228,8 @@ def interpolate(
     ):
         """Interpolate values onto this mesh from a given dataset.
 
-        The input dataset is typically a point cloud. Only point data from
-        the source mesh will be interpolated onto points of this mesh. Whether
+        The ``target`` dataset is typically a point cloud. Only point data from
+        the ``target`` mesh will be interpolated onto points of this mesh. Whether
         preexisting point and cell data of this mesh are preserved in the
         output can be customized with the ``pass_point_data`` and
         ``pass_cell_data`` parameters.
@@ -3188,6 +3238,10 @@ def interpolate(
         ``radius`` parameters to adjust this kernel. You can also switch this
         kernel to use an N closest points approach.
 
+        If the cell topology is more useful for interpolating, e.g. from a
+        discretized FEM or CFD simulation, use
+        :func:`pyvista.DataSetFilters.sample` instead.
+
         Parameters
         ----------
         target : pyvista.DataSet
@@ -3237,6 +3291,10 @@ def interpolate(
         pyvista.DataSet
             Interpolated dataset.  Return type matches input.
 
+        See Also
+        --------
+        pyvista.DataSetFilters.sample
+
         Examples
         --------
         Interpolate the values of 5 points onto a sample plane.


Test patch:
diff --git a/tests/filters/test_dataset_filters.py b/tests/filters/test_dataset_filters.py
--- a/tests/filters/test_dataset_filters.py
+++ b/tests/filters/test_dataset_filters.py
@@ -11,7 +11,7 @@
 from pyvista.core import _vtk_core
 from pyvista.core.celltype import CellType
 from pyvista.core.errors import NotAllTrianglesError, VTKVersionError
-from pyvista.errors import MissingDataError
+from pyvista.errors import MissingDataError, PyVistaDeprecationWarning
 
 normals = ['x', 'y', '-z', (1, 1, 1), (3.3, 5.4, 0.8)]
 
@@ -1145,17 +1145,24 @@ def test_smooth_taubin(uniform):
     assert np.allclose(smooth_inplace.points, smoothed.points)
 
 
-def test_resample():
+def test_sample():
     mesh = pyvista.Sphere(center=(4.5, 4.5, 4.5), radius=4.5)
     data_to_probe = examples.load_uniform()
-    result = mesh.sample(data_to_probe, progress_bar=True)
-    name = 'Spatial Point Data'
-    assert name in result.array_names
-    assert isinstance(result, type(mesh))
-    result = mesh.sample(data_to_probe, tolerance=1.0, progress_bar=True)
-    name = 'Spatial Point Data'
-    assert name in result.array_names
-    assert isinstance(result, type(mesh))
+
+    def sample_test(**kwargs):
+        """Test `sample` with kwargs."""
+        result = mesh.sample(data_to_probe, **kwargs)
+        name = 'Spatial Point Data'
+        assert name in result.array_names
+        assert isinstance(result, type(mesh))
+
+    sample_test()
+    sample_test(tolerance=1.0)
+    sample_test(progress_bar=True)
+    sample_test(categorical=True)
+    sample_test(locator=_vtk_core.vtkStaticCellLocator())
+    sample_test(pass_cell_data=False)
+    sample_test(pass_point_data=False)
 
 
 @pytest.mark.parametrize('use_points', [True, False])
@@ -1168,9 +1175,10 @@ def test_probe(categorical, use_points, locator):
         dataset = np.array(mesh.points)
     else:
         dataset = mesh
-    result = data_to_probe.probe(
-        dataset, tolerance=1e-5, categorical=categorical, progress_bar=True, locator=locator
-    )
+    with pytest.warns(PyVistaDeprecationWarning):
+        result = data_to_probe.probe(
+            dataset, tolerance=1e-5, categorical=categorical, progress_bar=True, locator=locator
+        )
     name = 'Spatial Point Data'
     assert name in result.array_names
     assert isinstance(result, type(mesh))