Repository: pyvista/pyvista
Pull request #7354: Fix cell quality size measures
Author: login: user27182
Labels: name: bug
description: Uh-oh! Something isn't working as expected.
Created: 2025-03-28T00:09:26Z | Merged: 2025-03-29T15:54:50Z

### Overview

All of the `size`-based measures in the `cell_quality` filter currently do not work (they return all zeros, see https://gitlab.kitware.com/vtk/vtk/-/issues/19645).

This PR fixes this by using `vtkMeshQuality` first to compute the necessary average cell sizes required by `vtkCellQuality` for the computation to work.


Diff:
diff --git a/pyvista/core/_vtk_core.py b/pyvista/core/_vtk_core.py
index 55af0c8f47..f25fd6bd4a 100644
--- a/pyvista/core/_vtk_core.py
+++ b/pyvista/core/_vtk_core.py
@@ -444,6 +444,7 @@
 from vtkmodules.vtkFiltersTexture import vtkTextureMapToSphere as vtkTextureMapToSphere
 from vtkmodules.vtkFiltersVerdict import vtkCellQuality as vtkCellQuality
 from vtkmodules.vtkFiltersVerdict import vtkCellSizeFilter as vtkCellSizeFilter
+from vtkmodules.vtkFiltersVerdict import vtkMeshQuality as vtkMeshQuality
 
 with contextlib.suppress(ImportError):
     from vtkmodules.vtkFiltersVerdict import vtkBoundaryMeshQuality as vtkBoundaryMeshQuality
diff --git a/pyvista/core/filters/data_object.py b/pyvista/core/filters/data_object.py
index dc91cb9262..7eb3320efe 100644
--- a/pyvista/core/filters/data_object.py
+++ b/pyvista/core/filters/data_object.py
@@ -2924,25 +2924,41 @@ def _dataset_cell_quality(  # type: ignore[misc]
         CELL_QUALITY = 'CellQuality'
 
         alg = _vtk.vtkCellQuality()
-        alg.SetInputData(self)
         alg.SetUndefinedQuality(null_value)
+
+        if 'size' in ''.join(measures_requested):
+            # Need to compute mesh quality statistics to get average cell size.
+            # We only need to do this once. This will create field data arrays:
+            # 'TriArea', 'QuadArea', 'TetVolume', 'PyrVolume', 'WedgeVolume', 'HexVolume'
+            # which are used later by vtkCellQuality
+            mesh_quality = _vtk.vtkMeshQuality()
+            mesh_quality.SaveCellQualityOff()
+            mesh_quality.SetInputData(self)
+            # Setting any 'Size' measure for any cell (tri, quad, etc.) is sufficient to
+            # ensure all necessary base stats are computed for all cell types and for
+            # all 'Size' measures
+            mesh_quality.SetTriangleQualityMeasureToShapeAndSize()
+            mesh_quality.Update()
+
+            alg.SetInputDataObject(mesh_quality.GetOutput())
+        else:
+            alg.SetInputDataObject(self)
+
         output = self.copy()
 
-        # Disable VTK warnings/errors generated from computing invalid measures
-        with pyvista.vtk_verbosity('off'):
-            # Compute all measures
-            for measure in measures_requested:
-                # Set measure and update
-                getattr(alg, measures_available[measure])()
-                _update_alg(alg, progress_bar, f"Computing Cell Quality '{measure}'")
-
-                # Store the cell quality array with the output
-                cell_quality_array = _get_output(alg).cell_data[CELL_QUALITY]
-                if keep_valid_only and (
-                    np.max(cell_quality_array) == np.min(cell_quality_array) == null_value
-                ):
-                    continue
-                output.cell_data[measure] = cell_quality_array
+        # Compute all measures
+        for measure in measures_requested:
+            # Set measure and update
+            getattr(alg, measures_available[measure])()
+            _update_alg(alg, progress_bar, f"Computing Cell Quality '{measure}'")
+
+            # Store the cell quality array with the output
+            cell_quality_array = _get_output(alg).cell_data[CELL_QUALITY]
+            if keep_valid_only and (
+                np.max(cell_quality_array) == np.min(cell_quality_array) == null_value
+            ):
+                continue
+            output.cell_data[measure] = cell_quality_array
         return output
 
 
diff --git a/tests/core/test_dataobject_filters.py b/tests/core/test_dataobject_filters.py
index 5ccd504be3..044a893bba 100644
--- a/tests/core/test_dataobject_filters.py
+++ b/tests/core/test_dataobject_filters.py
@@ -546,6 +546,21 @@ def test_cell_quality_measures(ant):
     assert qual.array_names == actual_measures
 
 
+@pytest.mark.parametrize(
+    'cell_mesh',
+    [
+        examples.cells.Triangle(),
+        examples.cells.Quadrilateral(),
+        examples.cells.Hexahedron(),
+        examples.cells.Tetrahedron(),
+    ],
+)
+@pytest.mark.parametrize('measure', ['relative_size_squared', 'shape_and_size'])
+def test_cell_quality_size_measures(cell_mesh, measure):
+    quality = cell_mesh.cell_quality(measure)
+    assert np.isclose(quality[measure][0], 1.0)
+
+
 def test_cell_quality_all_valid(ant):
     qual = ant.cell_quality('all_valid')
     assert AREA in qual.array_names


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR fixes the computation of size-based cell quality measures by ensuring the necessary mesh quality statistics are computed before the cell quality evaluation.  
- Added tests for new cell quality size measures using multiple cell types.  
- Updated the data object filter to compute average cell sizes via vtkMeshQuality when a size measure is requested.  
- Included the required vtkMeshQuality import in the VTK core module.

### Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

| File | Description |
| ---- | ----------- |
| tests/core/test_dataobject_filters.py | Added parameterized tests for cell quality size measures |
| pyvista/core/filters/data_object.py | Modified the cell quality filter logic to use vtkMeshQuality when a size measure is present |
| pyvista/core/_vtk_core.py | Added the vtkMeshQuality import to support the new functionality |


<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**pyvista/core/filters/data_object.py:2929**
* Using a generic substring check for 'size' in the measures_requested list may unintentionally trigger the branch for measures that include 'size' as a substring. Consider checking explicitly for the known size measure names to avoid potential false positives.
```
if 'size' in ''.join(measures_requested):
```
**pyvista/core/filters/data_object.py:2942**
* [nitpick] The call to mesh_quality.GetOutput() is not assigned or used. If this call is necessary to force an update or for side effects, consider adding a comment for clarity; otherwise, it may be redundant.
```
mesh_quality.GetOutput()
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T03:53:11Z
user: login: user27182
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T03:58:59Z
user: login: tkoyama010
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T12:56:35Z
user: login: tkoyama010
body: LGTM
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-29T12:56:59Z
user: login: tkoyama010
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-29T12:57:17Z

Review comments:
diff_hunk: @@ -2924,25 +2924,42 @@ def _dataset_cell_quality(  # type: ignore[misc]
         CELL_QUALITY = 'CellQuality'
 
         alg = _vtk.vtkCellQuality()
-        alg.SetInputData(self)
         alg.SetUndefinedQuality(null_value)
+
+        if 'size' in ''.join(measures_requested):
+            # Need to compute mesh quality statistics to get average cell size.
+            # We only need to do this once. This will create field data arrays:
+            # 'TriArea', 'QuadArea', 'TetVolume', 'PyrVolume', 'WedgeVolume', 'HexVolume'
+            # which are used later by vtkCellQuality
+            mesh_quality = _vtk.vtkMeshQuality()
+            mesh_quality.SaveCellQualityOff()
+            mesh_quality.SetInputData(self)
+            # Setting any 'Size' measure for any cell (tri, quad, etc.) is sufficient to
+            # ensure all necessary base stats are computed for all cell types and for
+            # all 'Size' measures
+            mesh_quality.SetTriangleQualityMeasureToShapeAndSize()
+            mesh_quality.Update()
+            mesh_quality.GetOutput()
path: pyvista/core/filters/data_object.py
user: login: user27182
body: Unused, let's remove (thanks copilot!)
```suggestion
```
created_at: 2025-03-28T03:58:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -546,6 +546,21 @@ def test_cell_quality_measures(ant):
     assert qual.array_names == actual_measures
 
 
+@pytest.mark.parametrize(
+    'cell_mesh',
+    [
+        examples.cells.Triangle(),
+        examples.cells.Quadrilateral(),
+        examples.cells.Hexahedron(),
+        examples.cells.Tetrahedron(),
+    ],
+)
+@pytest.mark.parametrize('measure', ['relative_size_squared', 'shape_and_size'])
+def test_cell_quality_size_measures(cell_mesh, measure):
+    qual = cell_mesh.cell_quality(measure)
+    assert np.isclose(qual[measure][0], 1.0)
path: tests/core/test_dataobject_filters.py
user: login: tkoyama010
body: ```suggestion
    quality = cell_mesh.cell_quality(measure)
    assert np.isclose(quality[measure][0], 1.0)
```
created_at: 2025-03-29T12:54:07Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-03-28T00:52:10Z
body: ## [Codecov](https://app.codecov.io/gh/pyvista/pyvista/pull/7354?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 96.04%. Comparing base [(`4945198`)](https://app.codecov.io/gh/pyvista/pyvista/commit/4945198183ca03f678373a30e5d0d6583b896701?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) to head [(`093572e`)](https://app.codecov.io/gh/pyvista/pyvista/commit/093572e4c1b77143d0bb89e7a119480b0ac933f7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista).
> Report is 1 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #7354   +/-   ##
=======================================
  Coverage   96.04%   96.04%           
=======================================
  Files         147      147           
  Lines       30357    30364    +7     
  Branches     3984     3985    +1     
=======================================
+ Hits        29157    29164    +7     
  Misses        570      570           
  Partials      630      630           
```

</details>
author_association: NONE
user: login: pyvista-bot
created_at: 2025-03-28T07:42:34Z
body: <!-- NETLIFY DEPLOY COMMENT GENERATED BY ACTIONS_NETLIFY - APP ID SHA256: d3574f413fe51079937885070831e027ca5b10f7facb9e609494e0a481a31f79 -->
🚀 Deployed on https://67e817e93c386273965d0c82--pyvista-dev.netlify.app
author_association: CONTRIBUTOR