Repository: pyvista/pyvista

Issue:
Enhancements for sample filter
### Describe the feature you would like to be added.

- Enable for composite data sets
- Expose pass partial arrays setting in that case
- Look into whether ComputeTolerance needs to be set when setting tolerance
- SnapToCellWithClosestPoint functionality to enable resampling


Lower Priority
- SetCategoricalData.
- PassFieldArrays. Could be useful to avoid name clashes.
- MarkBlankPointsandCells.  I'm not sure why anyone would ever want to turn this off, but it is an option in vtk


This comes out of the work in #4648


### Links to VTK Documentation, Examples, or Class Definitions.

https://vtk.org/doc/nightly/html/classvtkResampleWithDataSet.html#ac433a25e3e096c999bd99b06302f8171

### Pseudocode or Screenshots

_No response_

Discussion:
MatthewFlamm: In my testing the default locator, i.e. the dataset `FindCell`, is not robust and often leads to not finding a cell for a given point that is clearly within the dataset.  When using `vtkStaticCellLocator` it gives more robust finding.  But I'm not sure if this the best option for all cases.  I think it would be better to have a default that is more robust, but I'm not as familiar with the intricacies of each locator and how they might interact with all the dataset types.  
tkoyama010: Thanks for this. This is a great addition.

Patch:
diff --git a/pyvista/core/filters/composite.py b/pyvista/core/filters/composite.py
index 72b464ed..1bbd8085 100644
--- a/pyvista/core/filters/composite.py
+++ b/pyvista/core/filters/composite.py
@@ -107,6 +107,8 @@ class CompositeFilters:
 
     point_data_to_cell_data = DataSetFilters.point_data_to_cell_data
 
+    sample = DataSetFilters.sample
+
     triangulate = DataSetFilters.triangulate
 
     def outline(self, generate_faces=False, nested=False, progress_bar=False):
diff --git a/pyvista/core/filters/data_set.py b/pyvista/core/filters/data_set.py
index 0df01b58..99bd8f2e 100644
--- a/pyvista/core/filters/data_set.py
+++ b/pyvista/core/filters/data_set.py
@@ -3146,6 +3146,8 @@ class DataSetFilters:
         categorical=False,
         progress_bar=False,
         locator=None,
+        pass_field_data=True,
+        mark_blank=True,
     ):
         """Resample array data from a passed mesh onto this mesh.
 
@@ -3156,6 +3158,10 @@ class DataSetFilters:
         weighting for nearby points.  If there is cell topology, `sample` is
         usually preferred.
 
+        The point data 'vtkValidPointMask' stores whether the point could be sampled
+        with a value of 1 meaning successful sampling. And a value of 0 means
+        unsuccessful.
+
         This uses :class:`vtk.vtkResampleWithDataSet`.
 
         Parameters
@@ -3183,9 +3189,21 @@ class DataSetFilters:
         progress_bar : bool, default: False
             Display a progress bar to indicate progress.
 
-        locator : vtkAbstractCellLocator, optional
+        locator : vtkAbstractCellLocator or str, optional
             Prototype cell locator to perform the ``FindCell()``
             operation.  Default uses the DataSet ``FindCell`` method.
+            Valid strings with mapping to vtk cell locators are
+
+                * 'cell' - vtkCellLocator
+                * 'cell_tree' - vtkCellTreeLocator
+                * 'obb_tree' - vtkOBBTree
+                * 'static_cell' - vtkStaticCellLocator
+
+        pass_field_data : bool, default: True
+            Preserve source mesh's original field data arrays.
+
+        mark_blank : bool, default: True
+            Whether to mark blank points and cells in "vtkGhostType".
 
         Returns
         -------
@@ -3229,11 +3247,28 @@ class DataSetFilters:
         alg.SetSourceData(target)
         alg.SetPassCellArrays(pass_cell_data)
         alg.SetPassPointArrays(pass_point_data)
+        alg.SetPassFieldArrays(pass_field_data)
+
+        alg.SetMarkBlankPointsAndCells(mark_blank)
         alg.SetCategoricalData(categorical)
+
         if tolerance is not None:
             alg.SetComputeTolerance(False)
             alg.SetTolerance(tolerance)
         if locator:
+            if isinstance(locator, str):
+                locator_map = {
+                    "cell": _vtk.vtkCellLocator(),
+                    "cell_tree": _vtk.vtkCellTreeLocator(),
+                    "obb_tree": _vtk.vtkOBBTree(),
+                    "static_cell": _vtk.vtkStaticCellLocator(),
+                }
+                try:
+                    locator = locator_map[locator]
+                except KeyError as err:
+                    raise ValueError(
+                        f"locator must be a string from {locator_map.keys()}, got {locator}"
+                    ) from err
             alg.SetCellLocatorPrototype(locator)
 
         _update_alg(alg, progress_bar, 'Resampling array Data from a Passed Mesh onto Mesh')
diff --git a/requirements_docs.txt b/requirements_docs.txt
index 363aa754..117d373b 100644
--- a/requirements_docs.txt
+++ b/requirements_docs.txt
@@ -10,7 +10,7 @@ jupyter_sphinx==0.4.0
 jupyterlab==4.0.5
 lxml==4.9.3
 meshio==5.3.4
-mypy==1.5.0
+mypy==1.5.1
 mypy-extensions==1.0.0
 numpydoc==1.5.0
 osmnx==1.6.0
@@ -31,7 +31,7 @@ sphinx-toolbox==3.5.0
 sphinxcontrib-asciinema==0.3.7
 sphinxcontrib-websupport==1.2.6
 sympy==1.12
-trame==3.2.1
+trame==3.2.2
 trame-vtk==2.5.8
 trame-vuetify==2.3.1
-trimesh==3.23.1
+trimesh==3.23.2


Test patch:
diff --git a/tests/core/test_dataset_filters.py b/tests/core/test_dataset_filters.py
index bf7db91a..ae2a932d 100644
--- a/tests/core/test_dataset_filters.py
+++ b/tests/core/test_dataset_filters.py
@@ -1165,8 +1165,63 @@ def test_sample():
     sample_test(progress_bar=True)
     sample_test(categorical=True)
     sample_test(locator=_vtk_core.vtkStaticCellLocator())
+    for locator in ['cell', 'cell_tree', 'obb_tree', 'static_cell']:
+        sample_test(locator=locator)
+    with pytest.raises(ValueError):
+        sample_test(locator="invalid")
     sample_test(pass_cell_data=False)
     sample_test(pass_point_data=False)
+    sample_test(pass_field_data=False)
+
+
+def test_sample_composite():
+    mesh0 = pyvista.ImageData(
+        dimensions=(11, 11, 1), origin=(0.0, 0.0, 0.0), spacing=(1.0, 1.0, 1.0)
+    )
+    mesh1 = pyvista.ImageData(
+        dimensions=(11, 11, 1), origin=(10.0, 0.0, 0.0), spacing=(1.0, 1.0, 1.0)
+    )
+    mesh0["common_data"] = np.zeros(mesh0.n_points)
+    mesh1["common_data"] = np.ones(mesh1.n_points)
+    mesh0["partial_data"] = np.zeros(mesh0.n_points)
+
+    composite = pyvista.MultiBlock([mesh0, mesh1])
+
+    probe_points = pyvista.PolyData(
+        [
+            [5.0, 5.0, 0.0],
+            [15.0, 5.0, 0.0],
+            [25.0, 5.0, 0.0],  # outside domain
+        ]
+    )
+
+    result = probe_points.sample(composite)
+    assert "common_data" in result.point_data
+    # Need pass partial arrays?
+    assert "partial_data" not in result.point_data
+    assert "vtkValidPointMask" in result.point_data
+    assert "vtkGhostType" in result.point_data
+    # data outside domain is 0
+    assert np.array_equal(result["common_data"], [0.0, 1.0, 0.0])
+    assert np.array_equal(result["vtkValidPointMask"], [1, 1, 0])
+
+    result = probe_points.sample(composite, mark_blank=False)
+    assert "vtkGhostType" not in result.point_data
+
+    small_mesh_0 = pyvista.ImageData(
+        dimensions=(6, 6, 1), origin=(0.0, 0.0, 0.0), spacing=(1.0, 1.0, 1.0)
+    )
+    small_mesh_1 = pyvista.ImageData(
+        dimensions=(6, 6, 1), origin=(10.0, 0.0, 0.0), spacing=(1.0, 1.0, 1.0)
+    )
+
+    probe_composite = pyvista.MultiBlock([small_mesh_0, small_mesh_1])
+    result = probe_composite.sample(composite)
+    assert "common_data" in result[0].point_data
+    # Need pass partial arrays?
+    assert "partial_data" not in result[0].point_data
+    assert "vtkValidPointMask" in result[0].point_data
+    assert "vtkGhostType" in result[0].point_data
 
 
 @pytest.mark.parametrize('use_points', [True, False])