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:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pyvista/pyvista/pull/4648?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) Report
> Merging [#4648](https://app.codecov.io/gh/pyvista/pyvista/pull/4648?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) (6f325d2) into [main](https://app.codecov.io/gh/pyvista/pyvista/commit/d804a93a3bcae250c74a9f0f7c37fbc8bf002011?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) (d804a93) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##             main    #4648   +/-   ##
=======================================
  Coverage   95.47%   95.47%           
=======================================
  Files         127      127           
  Lines       21193    21194    +1     
=======================================
+ Hits        20234    20235    +1     
  Misses        959      959           
```

banesullivan: I thought we converged on implementing `eborp()`? 😄 
MatthewFlamm: The only thing that `sample` does not support today that `probe` supports is choosing the Cell Locator.  But, I'm not sure that this is working for probe today anyway.  From `vtkProbeFilter` [docs](https://vtk.org/doc/nightly/html/classvtkProbeFilter.html#a3f8ecf7772d00409715704b2a36b8216), 

> (A prototype is used as an object factory to instantiate an instance of the prototype to perform the FindCell() operation). If a prototype, and a [vtkFindCellStrategy](https://vtk.org/doc/nightly/html/classvtkFindCellStrategy.html) are not defined, the [vtkDataSet::FindCell()](https://vtk.org/doc/nightly/html/classvtkDataSet.html#a2221c10d3c4cca44e82c5ef70e4e1cbd) is used. If a [vtkFindCellStrategy](https://vtk.org/doc/nightly/html/classvtkFindCellStrategy.html) is not defined, then the prototype is used.

Note this part: "If a prototype, and a [vtkFindCellStrategy](https://vtk.org/doc/nightly/html/classvtkFindCellStrategy.html) are not defined,", which in my readings means that you have to set both of these to change the behavior.  Right now we only set the prototype cell locator and not the vtkFindCellStrategy.

If this is right, then I propose not implementing this functionality for `sample` at this time.
banesullivan: I'm running into a use case where `sample()` cannot reproduce the results that `probe()` is giving:

```py
import pyvista as pv
import numpy as np

points = pv.wrap(pv.Sphere().points)
points['my data'] = np.arange(points.n_points)

surf = points.reconstruct_surface().probe(points)
```

`sample()` fails whereas `probe()` works out of the box. Perhaps this is a parameters issue
MatthewFlamm: > I'm running into a use case where `sample()` cannot reproduce the results that `probe()` is giving:
> 
> ```python
> import pyvista as pv
> import numpy as np
> 
> points = pv.wrap(pv.Sphere().points)
> points['my data'] = np.arange(points.n_points)
> 
> surf = points.reconstruct_surface().probe(points)
> ```
> 
> `sample()` fails whereas `probe()` works out of the box. Perhaps this is a parameters issue

Something feels off here.  `mesh2.probe(mesh1)` means probe data from mesh2 and put it on points of mesh1.  So you take the data from the reconstructed mesh, which has no data, and try to put it onto the original mesh in this case.  The default of `probe` and `sample` is to continue passing the cell and point data of the source mesh (the mesh which we are putting data onto) to the result.  So you end up with the original data back _without_ a reconstructed surface.  I think you want the reverse, i.e. `points.probe(points.reconstruct_surface())`.  Did you do `surf =p oints.reconstruct_surface().sample(points)` (note that this is not the same as your example since these two filters are swapped) or `surf = points.sample(points.reconstruct_surface())`, which gives me the same as the probe example you gave (although flawed).

These filters both aren't working in the intended because they are cell-wise interpolation, yet you are trying to probe/sample from a point cloud that has no cell topology.  There is a snap to closest cell option that we could expose for the future which would fix this issue for this type of usage..

banesullivan: Crap, I messed this up. Yes, thanks for all of this work clarifying these APIs because these are all very messy and inconsistent (to the point that I got quite confused)
MatthewFlamm: This is ready for review.  The only thing I considered but didn't implement yet was to create a new example to contrast the two filters.

Some future looking thoughts for `sample` filter (mostly so I can reference them later):

- Expose `SnapToClosestCell`, which would be useful for the use case above.   Add as an example at that time.
- Wrap as a filter for MultiBlock since vtkResampleWithDataSet supports this.
- Check that we have exposed all the other useful options.



Patch:
diff --git a/examples/01-filter/interpolate.py b/examples/01-filter/interpolate.py
index eadae345..24d9ff31 100644
--- 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
index f0a55560..663b76d7 100644
--- 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 @@ from pyvista import examples
 ###############################################################################
 # 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/examples/02-plot/point-clouds.py b/examples/02-plot/point-clouds.py
index c90cf709..b9c9f91e 100644
--- a/examples/02-plot/point-clouds.py
+++ b/examples/02-plot/point-clouds.py
@@ -4,7 +4,7 @@
 Plotting Point Clouds
 ~~~~~~~~~~~~~~~~~~~~~
 This example shows you how to plot point clouds using PyVista using both the
-``'point'`` and ``'point_gaussian'`` styles.
+``'points'`` and ``'points_gaussian'`` styles.
 
 """
 
@@ -61,7 +61,7 @@ pv.plot(points, scalars=rgba, render_points_as_spheres=True, point_size=20, cpos
 ###############################################################################
 # Point Cloud Plot Styles
 # ~~~~~~~~~~~~~~~~~~~~~~~
-# PyVista supports the ``'point_gaussian'`` style, which renders points as
+# PyVista supports the ``'points_gaussian'`` style, which renders points as
 # individual soft sprites. You have the option of displaying these as tight
 # "spheres" using ``render_points_as_spheres=True`` (default), or disabling it
 # to create softer points at the expense of render performance.
diff --git a/pyvista/core/filters/data_set.py b/pyvista/core/filters/data_set.py
index f81b1754..54532454 100644
--- a/pyvista/core/filters/data_set.py
+++ b/pyvista/core/filters/data_set.py
@@ -8,7 +8,12 @@ import numpy as np
 
 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 @@ class DataSetFilters:
     ):
         """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 @@ class DataSetFilters:
         >>> 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 @@ class DataSetFilters:
         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 @@ class DataSetFilters:
         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 @@ class DataSetFilters:
         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 @@ class DataSetFilters:
     ):
         """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 @@ class DataSetFilters:
         ``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 @@ class DataSetFilters:
         pyvista.DataSet
             Interpolated dataset.  Return type matches input.
 
+        See Also
+        --------
+        pyvista.DataSetFilters.sample
+
         Examples
         --------
         Interpolate the values of 5 points onto a sample plane.
diff --git a/pyvista/core/utilities/geometric_objects.py b/pyvista/core/utilities/geometric_objects.py
index 57766ef2..a1890bd7 100644
--- a/pyvista/core/utilities/geometric_objects.py
+++ b/pyvista/core/utilities/geometric_objects.py
@@ -507,7 +507,11 @@ def MultipleLines(points=[[-0.5, 0.0, 0.0], [0.5, 0.0, 0.0]]):
     >>> mesh = pyvista.MultipleLines(
     ...     points=[[0, 0, 0], [1, 1, 1], [0, 0, 1]]
     ... )
-    >>> mesh.plot(color='k', line_width=10)
+    >>> plotter = pyvista.Plotter()
+    >>> actor = plotter.add_mesh(mesh, color='k', line_width=10)
+    >>> plotter.camera.azimuth = 45
+    >>> plotter.camera.zoom(0.8)
+    >>> plotter.show()
     """
     points, _ = _coerce_pointslike_arg(points)
     src = _vtk.vtkLineSource()
diff --git a/requirements_docs.txt b/requirements_docs.txt
index d2fab0e7..a42e26dd 100644
--- a/requirements_docs.txt
+++ b/requirements_docs.txt
@@ -7,7 +7,7 @@ imageio-ffmpeg==0.4.8
 ipygany==0.5.0
 ipyvtklink==0.2.3
 jupyter_sphinx==0.4.0
-jupyterlab==4.0.2
+jupyterlab==4.0.3
 lxml==4.9.3
 meshio==5.3.4
 mypy==1.4.1
@@ -35,4 +35,4 @@ trame==2.5.0
 trame-client==2.9.4
 trame-server==2.11.4
 trame-vtk==2.5.4
-trimesh==3.22.3
+trimesh==3.22.4


Test patch:
diff --git a/tests/filters/test_dataset_filters.py b/tests/filters/test_dataset_filters.py
index a0821079..60e1626e 100644
--- a/tests/filters/test_dataset_filters.py
+++ b/tests/filters/test_dataset_filters.py
@@ -11,7 +11,7 @@ from pyvista import examples
 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))