From b33ce6a5f20ed8b169fb5ca4a9ae00363fb039c1 Mon Sep 17 00:00:00 2001 From: "paul.profizi" Date: Fri, 25 Oct 2024 15:14:15 +0200 Subject: [PATCH 01/21] Add a vtk_mesh_is_valid helper to check validity of VTK meshes. Add a 'check_validity' argument to the dpf_mesh_to_vtk function. Signed-off-by: paul.profizi --- src/ansys/dpf/core/vtk_helper.py | 108 +++++++++++++++++++++++++++++-- 1 file changed, 104 insertions(+), 4 deletions(-) diff --git a/src/ansys/dpf/core/vtk_helper.py b/src/ansys/dpf/core/vtk_helper.py index d0a0db37d9..668b876a9c 100644 --- a/src/ansys/dpf/core/vtk_helper.py +++ b/src/ansys/dpf/core/vtk_helper.py @@ -19,8 +19,10 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import warnings import numpy as np +import pyvista import pyvista as pv from typing import Union from vtk import ( @@ -362,6 +364,7 @@ def dpf_mesh_to_vtk( mesh: dpf.MeshedRegion, nodes: Union[dpf.Field, None] = None, as_linear: bool = True, + check_validity: bool = False, ) -> pv.UnstructuredGrid: """Return a pyvista UnstructuredGrid given a pydpf MeshedRegion. @@ -376,8 +379,8 @@ def dpf_mesh_to_vtk( as_linear: Export quadratic surface elements as linear. - export_faces: - Whether to export face elements along with volume elements for fluid meshes. + check_validity: + Whether to run the VTK cell validity check on the generated mesh and warn if not valid. Returns ------- @@ -385,9 +388,106 @@ def dpf_mesh_to_vtk( UnstructuredGrid corresponding to the DPF mesh. """ try: - return dpf_mesh_to_vtk_op(mesh, nodes, as_linear) + grid = dpf_mesh_to_vtk_op(mesh, nodes, as_linear) except (AttributeError, KeyError, errors.DPFServerException): - return dpf_mesh_to_vtk_py(mesh, nodes, as_linear) + grid = dpf_mesh_to_vtk_py(mesh, nodes, as_linear) + if check_validity: + valid, msg = vtk_mesh_is_valid(grid) + if not valid: + warnings.warn(f"\nVTK mesh validity check\n{msg}") + return grid + + +def vtk_mesh_is_valid(grid: pv.UnstructuredGrid, verbose: bool = False) -> (bool, str): + """Runs a vtk.CellValidator filter on the input grid. + + Parameters + ---------- + grid: + A vtk mesh to validate. + verbose: + Whether to print the complete validation. + + Returns + ------- + valid: + Whether the vtk mesh is valid according to the vtkCellValidator. + message: + Output message. + + """ + from enum import Enum + + from vtkmodules.vtkFiltersGeneral import vtkCellValidator + from vtkmodules.util.numpy_support import vtk_to_numpy + + # Prepare the Enum of possible validity states + class State(Enum): + Valid = 0 + WrongNumberOfPoints = (1,) + IntersectingEdges = (2,) + IntersectingFaces = (4,) + NoncontiguousEdges = (8,) + Nonconvex = (16,) + FacesAreOrientedIncorrectly = (32,) + + # Run the cell validator + cell_validator = vtkCellValidator() + cell_validator.SetInputData(grid) + cell_validator.Update() + # Get the states for all cells as a numpy array + cell_states = vtk_to_numpy( + cell_validator.GetUnstructuredGridOutput().GetCellData().GetArray("ValidityState") + ) + # Check for invalid states + elem_with_wrong_number_of_nodes = np.where(cell_states == State.WrongNumberOfPoints.value)[0] + elem_with_intersecting_edges = np.where(cell_states == State.IntersectingEdges.value)[0] + elem_with_intersecting_faces = np.where(cell_states == State.IntersectingFaces.value)[0] + elem_with_noncontiguous_edges = np.where(cell_states == State.NoncontiguousEdges.value)[0] + elem_with_nonconvex_shape = np.where(cell_states == State.Nonconvex.value)[0] + elem_with_badly_oriented_faces = np.where( + cell_states == State.FacesAreOrientedIncorrectly.value + )[0] + # Build list of number of elements failing each test + failing_elements_number = [ + len(elem_with_wrong_number_of_nodes), + len(elem_with_intersecting_edges), + len(elem_with_intersecting_faces), + len(elem_with_noncontiguous_edges), + len(elem_with_nonconvex_shape), + len(elem_with_badly_oriented_faces), + ] + # Define whether mesh is valid + mesh_is_valid = np.sum(failing_elements_number) == 0 + # Build output message + out_msg = "" + if mesh_is_valid: + out_msg += "Mesh is valid." + else: + out_msg += "Mesh is invalid because of (by index):\n" + if failing_elements_number[0] > 0: + out_msg += ( + f" - {failing_elements_number[0]} elements with the wrong number of points:\n" + ) + out_msg += f" {elem_with_wrong_number_of_nodes}\n" + if failing_elements_number[1] > 0: + out_msg += f" - {failing_elements_number[1]} elements with intersecting edges:\n" + out_msg += f" {elem_with_intersecting_edges}\n" + if failing_elements_number[2] > 0: + out_msg += f" - {failing_elements_number[2]} elements with intersecting faces:\n" + out_msg += f" {elem_with_intersecting_faces}\n" + if failing_elements_number[3] > 0: + out_msg += f" - {failing_elements_number[3]} elements with non contiguous edges:\n" + out_msg += f" {elem_with_noncontiguous_edges}\n" + if failing_elements_number[4] > 0: + out_msg += f" - {failing_elements_number[4]} elements with non convex shape:\n" + out_msg += f" {elem_with_nonconvex_shape}\n" + if failing_elements_number[5] > 0: + out_msg += f" - {failing_elements_number[5]} elements with bad face orientations:\n" + out_msg += f" {elem_with_badly_oriented_faces}\n" + if verbose: + print(out_msg) + return mesh_is_valid, out_msg def vtk_update_coordinates(vtk_grid, coordinates_array): From 3835a76d66beb4c2b65d97d277e6424896c2868c Mon Sep 17 00:00:00 2001 From: "paul.profizi" Date: Fri, 25 Oct 2024 15:29:11 +0200 Subject: [PATCH 02/21] Return validity grid Signed-off-by: paul.profizi --- src/ansys/dpf/core/vtk_helper.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/ansys/dpf/core/vtk_helper.py b/src/ansys/dpf/core/vtk_helper.py index 668b876a9c..5bd8e88c68 100644 --- a/src/ansys/dpf/core/vtk_helper.py +++ b/src/ansys/dpf/core/vtk_helper.py @@ -392,13 +392,15 @@ def dpf_mesh_to_vtk( except (AttributeError, KeyError, errors.DPFServerException): grid = dpf_mesh_to_vtk_py(mesh, nodes, as_linear) if check_validity: - valid, msg = vtk_mesh_is_valid(grid) + valid, msg, _ = vtk_mesh_is_valid(grid) if not valid: warnings.warn(f"\nVTK mesh validity check\n{msg}") return grid -def vtk_mesh_is_valid(grid: pv.UnstructuredGrid, verbose: bool = False) -> (bool, str): +def vtk_mesh_is_valid( + grid: pv.UnstructuredGrid, verbose: bool = False +) -> tuple[bool, str, pv.UnstructuredGrid]: """Runs a vtk.CellValidator filter on the input grid. Parameters @@ -414,6 +416,8 @@ def vtk_mesh_is_valid(grid: pv.UnstructuredGrid, verbose: bool = False) -> (bool Whether the vtk mesh is valid according to the vtkCellValidator. message: Output message. + validity_grid: + A copy of the original grid, with validity fields. """ from enum import Enum @@ -436,9 +440,8 @@ class State(Enum): cell_validator.SetInputData(grid) cell_validator.Update() # Get the states for all cells as a numpy array - cell_states = vtk_to_numpy( - cell_validator.GetUnstructuredGridOutput().GetCellData().GetArray("ValidityState") - ) + validity_grid = cell_validator.GetUnstructuredGridOutput() + cell_states = vtk_to_numpy(validity_grid.GetCellData().GetArray("ValidityState")) # Check for invalid states elem_with_wrong_number_of_nodes = np.where(cell_states == State.WrongNumberOfPoints.value)[0] elem_with_intersecting_edges = np.where(cell_states == State.IntersectingEdges.value)[0] @@ -487,7 +490,7 @@ class State(Enum): out_msg += f" {elem_with_badly_oriented_faces}\n" if verbose: print(out_msg) - return mesh_is_valid, out_msg + return mesh_is_valid, out_msg, pv.UnstructuredGrid(validity_grid) def vtk_update_coordinates(vtk_grid, coordinates_array): From 8b08347c00e4051414fe9021cba66768ebe65173 Mon Sep 17 00:00:00 2001 From: "paul.profizi" Date: Fri, 25 Oct 2024 15:31:28 +0200 Subject: [PATCH 03/21] Return validity grid Signed-off-by: paul.profizi --- src/ansys/dpf/core/vtk_helper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ansys/dpf/core/vtk_helper.py b/src/ansys/dpf/core/vtk_helper.py index 5bd8e88c68..3c610265b0 100644 --- a/src/ansys/dpf/core/vtk_helper.py +++ b/src/ansys/dpf/core/vtk_helper.py @@ -490,7 +490,9 @@ class State(Enum): out_msg += f" {elem_with_badly_oriented_faces}\n" if verbose: print(out_msg) - return mesh_is_valid, out_msg, pv.UnstructuredGrid(validity_grid) + validity_grid = pv.UnstructuredGrid(validity_grid) + validity_grid.set_active_scalars("ValidityState") + return mesh_is_valid, out_msg, validity_grid def vtk_update_coordinates(vtk_grid, coordinates_array): From 069bef08739066e2b5ece7da01aea43b9dc276cf Mon Sep 17 00:00:00 2001 From: Sean Ahern Date: Fri, 25 Oct 2024 11:12:51 -0400 Subject: [PATCH 04/21] fix: use bitwise operations for checking validity states --- src/ansys/dpf/core/vtk_helper.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/ansys/dpf/core/vtk_helper.py b/src/ansys/dpf/core/vtk_helper.py index 3c610265b0..3d9fae1489 100644 --- a/src/ansys/dpf/core/vtk_helper.py +++ b/src/ansys/dpf/core/vtk_helper.py @@ -443,14 +443,13 @@ class State(Enum): validity_grid = cell_validator.GetUnstructuredGridOutput() cell_states = vtk_to_numpy(validity_grid.GetCellData().GetArray("ValidityState")) # Check for invalid states - elem_with_wrong_number_of_nodes = np.where(cell_states == State.WrongNumberOfPoints.value)[0] - elem_with_intersecting_edges = np.where(cell_states == State.IntersectingEdges.value)[0] - elem_with_intersecting_faces = np.where(cell_states == State.IntersectingFaces.value)[0] - elem_with_noncontiguous_edges = np.where(cell_states == State.NoncontiguousEdges.value)[0] - elem_with_nonconvex_shape = np.where(cell_states == State.Nonconvex.value)[0] - elem_with_badly_oriented_faces = np.where( - cell_states == State.FacesAreOrientedIncorrectly.value - )[0] + elem_with_wrong_number_of_nodes = np.where(cell_states & State.WrongNumberOfPoints.value)[0] + elem_with_intersecting_edges = np.where(cell_states & State.IntersectingEdges.value)[0] + elem_with_intersecting_faces = np.where(cell_states & State.IntersectingFaces.value)[0] + elem_with_noncontiguous_edges = np.where(cell_states & State.NoncontiguousEdges.value)[0] + elem_with_nonconvex_shape = np.where(cell_states & State.Nonconvex.value)[0] + elem_with_badly_oriented_faces = np.where(cell_states & State.FacesAreOrientedIncorrectly.value)[0] + # Build list of number of elements failing each test failing_elements_number = [ len(elem_with_wrong_number_of_nodes), From 469912990e710c202cf5dbb24dd2e1ffbb4bd8d4 Mon Sep 17 00:00:00 2001 From: Sean Ahern Date: Fri, 25 Oct 2024 13:30:14 -0400 Subject: [PATCH 05/21] chore: Reformatted lines to better fit line length limits --- src/ansys/dpf/core/vtk_helper.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/ansys/dpf/core/vtk_helper.py b/src/ansys/dpf/core/vtk_helper.py index 3d9fae1489..7349fac268 100644 --- a/src/ansys/dpf/core/vtk_helper.py +++ b/src/ansys/dpf/core/vtk_helper.py @@ -443,12 +443,18 @@ class State(Enum): validity_grid = cell_validator.GetUnstructuredGridOutput() cell_states = vtk_to_numpy(validity_grid.GetCellData().GetArray("ValidityState")) # Check for invalid states - elem_with_wrong_number_of_nodes = np.where(cell_states & State.WrongNumberOfPoints.value)[0] - elem_with_intersecting_edges = np.where(cell_states & State.IntersectingEdges.value)[0] - elem_with_intersecting_faces = np.where(cell_states & State.IntersectingFaces.value)[0] - elem_with_noncontiguous_edges = np.where(cell_states & State.NoncontiguousEdges.value)[0] - elem_with_nonconvex_shape = np.where(cell_states & State.Nonconvex.value)[0] - elem_with_badly_oriented_faces = np.where(cell_states & State.FacesAreOrientedIncorrectly.value)[0] + elem_with_wrong_number_of_nodes = np.where( + cell_states & State.WrongNumberOfPoints.value)[0] + elem_with_intersecting_edges = np.where( + cell_states & State.IntersectingEdges.value)[0] + elem_with_intersecting_faces = np.where( + cell_states & State.IntersectingFaces.value)[0] + elem_with_noncontiguous_edges = np.where( + cell_states & State.NoncontiguousEdges.value)[0] + elem_with_nonconvex_shape = np.where( + cell_states & State.Nonconvex.value)[0] + elem_with_badly_oriented_faces = np.where( + cell_states & State.FacesAreOrientedIncorrectly.value)[0] # Build list of number of elements failing each test failing_elements_number = [ From 13b0de9001da82f1ce609c5255ee66627c9b6ad0 Mon Sep 17 00:00:00 2001 From: "paul.profizi" Date: Mon, 28 Oct 2024 09:40:32 +0100 Subject: [PATCH 06/21] Style check Signed-off-by: paul.profizi --- src/ansys/dpf/core/vtk_helper.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/ansys/dpf/core/vtk_helper.py b/src/ansys/dpf/core/vtk_helper.py index 7349fac268..fe064fe7e2 100644 --- a/src/ansys/dpf/core/vtk_helper.py +++ b/src/ansys/dpf/core/vtk_helper.py @@ -443,18 +443,14 @@ class State(Enum): validity_grid = cell_validator.GetUnstructuredGridOutput() cell_states = vtk_to_numpy(validity_grid.GetCellData().GetArray("ValidityState")) # Check for invalid states - elem_with_wrong_number_of_nodes = np.where( - cell_states & State.WrongNumberOfPoints.value)[0] - elem_with_intersecting_edges = np.where( - cell_states & State.IntersectingEdges.value)[0] - elem_with_intersecting_faces = np.where( - cell_states & State.IntersectingFaces.value)[0] - elem_with_noncontiguous_edges = np.where( - cell_states & State.NoncontiguousEdges.value)[0] - elem_with_nonconvex_shape = np.where( - cell_states & State.Nonconvex.value)[0] + elem_with_wrong_number_of_nodes = np.where(cell_states & State.WrongNumberOfPoints.value)[0] + elem_with_intersecting_edges = np.where(cell_states & State.IntersectingEdges.value)[0] + elem_with_intersecting_faces = np.where(cell_states & State.IntersectingFaces.value)[0] + elem_with_noncontiguous_edges = np.where(cell_states & State.NoncontiguousEdges.value)[0] + elem_with_nonconvex_shape = np.where(cell_states & State.Nonconvex.value)[0] elem_with_badly_oriented_faces = np.where( - cell_states & State.FacesAreOrientedIncorrectly.value)[0] + cell_states & State.FacesAreOrientedIncorrectly.value + )[0] # Build list of number of elements failing each test failing_elements_number = [ From ff96a9ce908d0ea0bcdc62c172e514eb6499ed20 Mon Sep 17 00:00:00 2001 From: "paul.profizi" Date: Tue, 29 Oct 2024 15:00:44 +0100 Subject: [PATCH 07/21] Add testing Signed-off-by: paul.profizi --- tests/test_vtk_translate.py | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_vtk_translate.py b/tests/test_vtk_translate.py index 89e0a62277..1066bfa24c 100644 --- a/tests/test_vtk_translate.py +++ b/tests/test_vtk_translate.py @@ -32,6 +32,7 @@ dpf_property_field_to_vtk, append_field_to_grid, append_fieldscontainer_to_grid, + vtk_mesh_is_valid, ) if misc.module_exists("pyvista"): @@ -226,3 +227,44 @@ def test_append_fields_container_to_grid(simple_rst, server_type): assert isinstance(ug, pv.UnstructuredGrid) assert "disp {'time': 1}" in ug.point_data.keys() assert "volume {'time': 1}" in ug.cell_data.keys() + + +@pytest.mark.skipif(not HAS_PYVISTA, reason="Please install pyvista") +def test_vtk_mesh_is_valid_polyhedron(): + # Element type is polyhedron + cell_types = [42] + + # Start with a valid element + nodes_1 = [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 0.5], + [1.0, 0.0, 0.5], + [0.0, 1.0, 0.5], + ] + cells_1 = [24, 5, 4, 4, 1, 2, 5, 4, 3, 0, 1, 4, 3, 2, 1, 0, 3, 3, 4, 5, 4, 5, 2, 0, 3] + grid = pv.UnstructuredGrid(cells_1, cell_types, nodes_1) + valid, msg, out_grid = vtk_mesh_is_valid(grid) + assert valid + + # Move one node + nodes_2 = [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, -0.05, 0.5], # Moved one node along Y axis + [1.0, 0.0, 0.5], + [0.0, 1.0, 0.5], + ] + grid = pv.UnstructuredGrid(cells_1, cell_types, nodes_2) + valid, msg, out_grid = vtk_mesh_is_valid(grid) + print(msg) + assert not valid # For some reason this element is found to be non-convex + + # Invert one face + cells_2 = [24, 5, 4, 4, 1, 2, 5, 4, 3, 0, 1, 4, 3, 2, 1, 0, 3, 5, 4, 3, 4, 5, 2, 0, 3] + grid = pv.UnstructuredGrid(cells_2, cell_types, nodes_1) + valid, msg, out_grid = vtk_mesh_is_valid(grid) + print(msg) + assert not valid # Non-convex AND bad face orientation From 3a9437e688a96861223704ad224f87932b1e345c Mon Sep 17 00:00:00 2001 From: Sean Ahern Date: Tue, 29 Oct 2024 10:45:36 -0400 Subject: [PATCH 08/21] Turn magic number into a named constant --- tests/test_vtk_translate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_vtk_translate.py b/tests/test_vtk_translate.py index 1066bfa24c..272eb9073d 100644 --- a/tests/test_vtk_translate.py +++ b/tests/test_vtk_translate.py @@ -232,7 +232,7 @@ def test_append_fields_container_to_grid(simple_rst, server_type): @pytest.mark.skipif(not HAS_PYVISTA, reason="Please install pyvista") def test_vtk_mesh_is_valid_polyhedron(): # Element type is polyhedron - cell_types = [42] + cell_types = [pv.CellType.POLYHEDRON] # Start with a valid element nodes_1 = [ From 80247dd22837ce5b524389939123f29c956ec760 Mon Sep 17 00:00:00 2001 From: "paul.profizi" Date: Wed, 30 Oct 2024 10:40:57 +0100 Subject: [PATCH 09/21] Propose new output structure for the validator Signed-off-by: paul.profizi --- src/ansys/dpf/core/vtk_helper.py | 81 +++++++++++++++++++++++--------- tests/test_vtk_translate.py | 37 +++++++++++---- 2 files changed, 88 insertions(+), 30 deletions(-) diff --git a/src/ansys/dpf/core/vtk_helper.py b/src/ansys/dpf/core/vtk_helper.py index fe064fe7e2..5147830c75 100644 --- a/src/ansys/dpf/core/vtk_helper.py +++ b/src/ansys/dpf/core/vtk_helper.py @@ -19,10 +19,9 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import warnings +from dataclasses import dataclass import numpy as np -import pyvista import pyvista as pv from typing import Union from vtk import ( @@ -45,6 +44,7 @@ VTK_WEDGE, vtkVersion, ) +import warnings import ansys.dpf.core as dpf from ansys.dpf.core import errors @@ -392,15 +392,47 @@ def dpf_mesh_to_vtk( except (AttributeError, KeyError, errors.DPFServerException): grid = dpf_mesh_to_vtk_py(mesh, nodes, as_linear) if check_validity: - valid, msg, _ = vtk_mesh_is_valid(grid) - if not valid: - warnings.warn(f"\nVTK mesh validity check\n{msg}") + validity = vtk_mesh_is_valid(grid) + if not validity.valid: + warnings.warn(f"\nVTK mesh validity check\n{validity.msg}") return grid -def vtk_mesh_is_valid( - grid: pv.UnstructuredGrid, verbose: bool = False -) -> tuple[bool, str, pv.UnstructuredGrid]: +@dataclass +class VTKMeshValidity: + """Dataclass containing the results of a call to vtk_mesh_is_valid. + valid: + Whether the vtk mesh is valid according to the vtkCellValidator. + message: + Output message. + validity_grid: + A copy of the original grid, with validity fields. + wrong_number_of_points: + List of indexes of elements with the wrong number of points. + intersecting_edges: + List of indexes of elements with intersecting edges. + intersecting_faces: + List of indexes of elements with intersecting faces. + non_contiguous_edges: + List of indexes of elements with non-contiguous edges. + non_convex: + List of indexes of elements with non-convex shape. + inverted_faces: + List of indexes of elements with inverted faces. + """ + + valid: bool + msg: str + grid: pv.UnstructuredGrid + wrong_number_of_points: np.ndarray + intersecting_edges: np.ndarray + intersecting_faces: np.ndarray + non_contiguous_edges: np.ndarray + non_convex: np.ndarray + inverted_faces: np.ndarray + + +def vtk_mesh_is_valid(grid: pv.UnstructuredGrid, verbose: bool = False) -> VTKMeshValidity: """Runs a vtk.CellValidator filter on the input grid. Parameters @@ -412,13 +444,8 @@ def vtk_mesh_is_valid( Returns ------- - valid: - Whether the vtk mesh is valid according to the vtkCellValidator. - message: - Output message. - validity_grid: - A copy of the original grid, with validity fields. - + validity: + A dataclass containing the results of the validator. """ from enum import Enum @@ -446,8 +473,8 @@ class State(Enum): elem_with_wrong_number_of_nodes = np.where(cell_states & State.WrongNumberOfPoints.value)[0] elem_with_intersecting_edges = np.where(cell_states & State.IntersectingEdges.value)[0] elem_with_intersecting_faces = np.where(cell_states & State.IntersectingFaces.value)[0] - elem_with_noncontiguous_edges = np.where(cell_states & State.NoncontiguousEdges.value)[0] - elem_with_nonconvex_shape = np.where(cell_states & State.Nonconvex.value)[0] + elem_with_non_contiguous_edges = np.where(cell_states & State.NoncontiguousEdges.value)[0] + elem_with_non_convex_shape = np.where(cell_states & State.Nonconvex.value)[0] elem_with_badly_oriented_faces = np.where( cell_states & State.FacesAreOrientedIncorrectly.value )[0] @@ -457,8 +484,8 @@ class State(Enum): len(elem_with_wrong_number_of_nodes), len(elem_with_intersecting_edges), len(elem_with_intersecting_faces), - len(elem_with_noncontiguous_edges), - len(elem_with_nonconvex_shape), + len(elem_with_non_contiguous_edges), + len(elem_with_non_convex_shape), len(elem_with_badly_oriented_faces), ] # Define whether mesh is valid @@ -482,10 +509,10 @@ class State(Enum): out_msg += f" {elem_with_intersecting_faces}\n" if failing_elements_number[3] > 0: out_msg += f" - {failing_elements_number[3]} elements with non contiguous edges:\n" - out_msg += f" {elem_with_noncontiguous_edges}\n" + out_msg += f" {elem_with_non_contiguous_edges}\n" if failing_elements_number[4] > 0: out_msg += f" - {failing_elements_number[4]} elements with non convex shape:\n" - out_msg += f" {elem_with_nonconvex_shape}\n" + out_msg += f" {elem_with_non_convex_shape}\n" if failing_elements_number[5] > 0: out_msg += f" - {failing_elements_number[5]} elements with bad face orientations:\n" out_msg += f" {elem_with_badly_oriented_faces}\n" @@ -493,7 +520,17 @@ class State(Enum): print(out_msg) validity_grid = pv.UnstructuredGrid(validity_grid) validity_grid.set_active_scalars("ValidityState") - return mesh_is_valid, out_msg, validity_grid + return VTKMeshValidity( + valid=mesh_is_valid, + msg=out_msg, + grid=validity_grid, + wrong_number_of_points=elem_with_wrong_number_of_nodes, + intersecting_edges=elem_with_intersecting_edges, + intersecting_faces=elem_with_intersecting_faces, + non_contiguous_edges=elem_with_non_contiguous_edges, + non_convex=elem_with_non_convex_shape, + inverted_faces=elem_with_badly_oriented_faces, + ) def vtk_update_coordinates(vtk_grid, coordinates_array): diff --git a/tests/test_vtk_translate.py b/tests/test_vtk_translate.py index 272eb9073d..a2dea39af6 100644 --- a/tests/test_vtk_translate.py +++ b/tests/test_vtk_translate.py @@ -245,8 +245,17 @@ def test_vtk_mesh_is_valid_polyhedron(): ] cells_1 = [24, 5, 4, 4, 1, 2, 5, 4, 3, 0, 1, 4, 3, 2, 1, 0, 3, 3, 4, 5, 4, 5, 2, 0, 3] grid = pv.UnstructuredGrid(cells_1, cell_types, nodes_1) - valid, msg, out_grid = vtk_mesh_is_valid(grid) - assert valid + validity = vtk_mesh_is_valid(grid) + print(validity) + assert validity.valid + assert "valid" in validity.msg + assert validity.grid.active_scalars_name == "ValidityState" + assert len(validity.wrong_number_of_points) == 0 + assert len(validity.intersecting_edges) == 0 + assert len(validity.intersecting_faces) == 0 + assert len(validity.non_contiguous_edges) == 0 + assert len(validity.non_convex) == 0 + assert len(validity.inverted_faces) == 0 # Move one node nodes_2 = [ @@ -258,13 +267,25 @@ def test_vtk_mesh_is_valid_polyhedron(): [0.0, 1.0, 0.5], ] grid = pv.UnstructuredGrid(cells_1, cell_types, nodes_2) - valid, msg, out_grid = vtk_mesh_is_valid(grid) - print(msg) - assert not valid # For some reason this element is found to be non-convex + validity = vtk_mesh_is_valid(grid) + print(validity) + assert not validity.valid # For some reason this element is found to be non-convex + assert len(validity.wrong_number_of_points) == 0 + assert len(validity.intersecting_edges) == 0 + assert len(validity.intersecting_faces) == 0 + assert len(validity.non_contiguous_edges) == 0 + assert len(validity.non_convex) == 1 + assert len(validity.inverted_faces) == 0 # Invert one face cells_2 = [24, 5, 4, 4, 1, 2, 5, 4, 3, 0, 1, 4, 3, 2, 1, 0, 3, 5, 4, 3, 4, 5, 2, 0, 3] grid = pv.UnstructuredGrid(cells_2, cell_types, nodes_1) - valid, msg, out_grid = vtk_mesh_is_valid(grid) - print(msg) - assert not valid # Non-convex AND bad face orientation + validity = vtk_mesh_is_valid(grid) + print(validity) + assert not validity.valid # Non-convex AND bad face orientation + assert len(validity.wrong_number_of_points) == 0 + assert len(validity.intersecting_edges) == 0 + assert len(validity.intersecting_faces) == 0 + assert len(validity.non_contiguous_edges) == 0 + assert len(validity.non_convex) == 1 + assert len(validity.inverted_faces) == 1 From a1a7b6cbc8c82d2edc800b66a3fc8fae7c79d6b9 Mon Sep 17 00:00:00 2001 From: Sean Ahern Date: Mon, 4 Nov 2024 09:03:25 -0500 Subject: [PATCH 10/21] Refactor connectivity for vtk mesh validity to improve readability --- tests/test_vtk_translate.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/tests/test_vtk_translate.py b/tests/test_vtk_translate.py index a2dea39af6..24baf22354 100644 --- a/tests/test_vtk_translate.py +++ b/tests/test_vtk_translate.py @@ -243,8 +243,19 @@ def test_vtk_mesh_is_valid_polyhedron(): [1.0, 0.0, 0.5], [0.0, 1.0, 0.5], ] - cells_1 = [24, 5, 4, 4, 1, 2, 5, 4, 3, 0, 1, 4, 3, 2, 1, 0, 3, 3, 4, 5, 4, 5, 2, 0, 3] - grid = pv.UnstructuredGrid(cells_1, cell_types, nodes_1) + cells_1 = [ + 5, + 4, 4, 1, 2, 5, + 4, + 3, 0, 1, 4, + 3, + 2, 1, 0, + 3, + 3, 4, 5, + 4, + 5, 2, 0, 3 + ] + grid = pv.UnstructuredGrid([len(cells_1), *cells_1], cell_types, nodes_1) validity = vtk_mesh_is_valid(grid) print(validity) assert validity.valid @@ -278,8 +289,19 @@ def test_vtk_mesh_is_valid_polyhedron(): assert len(validity.inverted_faces) == 0 # Invert one face - cells_2 = [24, 5, 4, 4, 1, 2, 5, 4, 3, 0, 1, 4, 3, 2, 1, 0, 3, 5, 4, 3, 4, 5, 2, 0, 3] - grid = pv.UnstructuredGrid(cells_2, cell_types, nodes_1) + cells_2 = [ + 5, + 4, 4, 1, 2, 5, + 4, + 3, 0, 1, 4, + 3, + 2, 1, 0, + 3, + 5, 4, 3, # Inverted face + 4, + 5, 2, 0, 3 + ] + grid = pv.UnstructuredGrid([len(cells_2), *cells_2], cell_types, nodes_1) validity = vtk_mesh_is_valid(grid) print(validity) assert not validity.valid # Non-convex AND bad face orientation From e45264d364047f11c978acc6dc86b935a6583b03 Mon Sep 17 00:00:00 2001 From: "paul.profizi" Date: Fri, 25 Oct 2024 15:14:15 +0200 Subject: [PATCH 11/21] Add a vtk_mesh_is_valid helper to check validity of VTK meshes. Add a 'check_validity' argument to the dpf_mesh_to_vtk function. Signed-off-by: paul.profizi --- src/ansys/dpf/core/vtk_helper.py | 108 +++++++++++++++++++++++++++++-- 1 file changed, 104 insertions(+), 4 deletions(-) diff --git a/src/ansys/dpf/core/vtk_helper.py b/src/ansys/dpf/core/vtk_helper.py index d0a0db37d9..668b876a9c 100644 --- a/src/ansys/dpf/core/vtk_helper.py +++ b/src/ansys/dpf/core/vtk_helper.py @@ -19,8 +19,10 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import warnings import numpy as np +import pyvista import pyvista as pv from typing import Union from vtk import ( @@ -362,6 +364,7 @@ def dpf_mesh_to_vtk( mesh: dpf.MeshedRegion, nodes: Union[dpf.Field, None] = None, as_linear: bool = True, + check_validity: bool = False, ) -> pv.UnstructuredGrid: """Return a pyvista UnstructuredGrid given a pydpf MeshedRegion. @@ -376,8 +379,8 @@ def dpf_mesh_to_vtk( as_linear: Export quadratic surface elements as linear. - export_faces: - Whether to export face elements along with volume elements for fluid meshes. + check_validity: + Whether to run the VTK cell validity check on the generated mesh and warn if not valid. Returns ------- @@ -385,9 +388,106 @@ def dpf_mesh_to_vtk( UnstructuredGrid corresponding to the DPF mesh. """ try: - return dpf_mesh_to_vtk_op(mesh, nodes, as_linear) + grid = dpf_mesh_to_vtk_op(mesh, nodes, as_linear) except (AttributeError, KeyError, errors.DPFServerException): - return dpf_mesh_to_vtk_py(mesh, nodes, as_linear) + grid = dpf_mesh_to_vtk_py(mesh, nodes, as_linear) + if check_validity: + valid, msg = vtk_mesh_is_valid(grid) + if not valid: + warnings.warn(f"\nVTK mesh validity check\n{msg}") + return grid + + +def vtk_mesh_is_valid(grid: pv.UnstructuredGrid, verbose: bool = False) -> (bool, str): + """Runs a vtk.CellValidator filter on the input grid. + + Parameters + ---------- + grid: + A vtk mesh to validate. + verbose: + Whether to print the complete validation. + + Returns + ------- + valid: + Whether the vtk mesh is valid according to the vtkCellValidator. + message: + Output message. + + """ + from enum import Enum + + from vtkmodules.vtkFiltersGeneral import vtkCellValidator + from vtkmodules.util.numpy_support import vtk_to_numpy + + # Prepare the Enum of possible validity states + class State(Enum): + Valid = 0 + WrongNumberOfPoints = (1,) + IntersectingEdges = (2,) + IntersectingFaces = (4,) + NoncontiguousEdges = (8,) + Nonconvex = (16,) + FacesAreOrientedIncorrectly = (32,) + + # Run the cell validator + cell_validator = vtkCellValidator() + cell_validator.SetInputData(grid) + cell_validator.Update() + # Get the states for all cells as a numpy array + cell_states = vtk_to_numpy( + cell_validator.GetUnstructuredGridOutput().GetCellData().GetArray("ValidityState") + ) + # Check for invalid states + elem_with_wrong_number_of_nodes = np.where(cell_states == State.WrongNumberOfPoints.value)[0] + elem_with_intersecting_edges = np.where(cell_states == State.IntersectingEdges.value)[0] + elem_with_intersecting_faces = np.where(cell_states == State.IntersectingFaces.value)[0] + elem_with_noncontiguous_edges = np.where(cell_states == State.NoncontiguousEdges.value)[0] + elem_with_nonconvex_shape = np.where(cell_states == State.Nonconvex.value)[0] + elem_with_badly_oriented_faces = np.where( + cell_states == State.FacesAreOrientedIncorrectly.value + )[0] + # Build list of number of elements failing each test + failing_elements_number = [ + len(elem_with_wrong_number_of_nodes), + len(elem_with_intersecting_edges), + len(elem_with_intersecting_faces), + len(elem_with_noncontiguous_edges), + len(elem_with_nonconvex_shape), + len(elem_with_badly_oriented_faces), + ] + # Define whether mesh is valid + mesh_is_valid = np.sum(failing_elements_number) == 0 + # Build output message + out_msg = "" + if mesh_is_valid: + out_msg += "Mesh is valid." + else: + out_msg += "Mesh is invalid because of (by index):\n" + if failing_elements_number[0] > 0: + out_msg += ( + f" - {failing_elements_number[0]} elements with the wrong number of points:\n" + ) + out_msg += f" {elem_with_wrong_number_of_nodes}\n" + if failing_elements_number[1] > 0: + out_msg += f" - {failing_elements_number[1]} elements with intersecting edges:\n" + out_msg += f" {elem_with_intersecting_edges}\n" + if failing_elements_number[2] > 0: + out_msg += f" - {failing_elements_number[2]} elements with intersecting faces:\n" + out_msg += f" {elem_with_intersecting_faces}\n" + if failing_elements_number[3] > 0: + out_msg += f" - {failing_elements_number[3]} elements with non contiguous edges:\n" + out_msg += f" {elem_with_noncontiguous_edges}\n" + if failing_elements_number[4] > 0: + out_msg += f" - {failing_elements_number[4]} elements with non convex shape:\n" + out_msg += f" {elem_with_nonconvex_shape}\n" + if failing_elements_number[5] > 0: + out_msg += f" - {failing_elements_number[5]} elements with bad face orientations:\n" + out_msg += f" {elem_with_badly_oriented_faces}\n" + if verbose: + print(out_msg) + return mesh_is_valid, out_msg def vtk_update_coordinates(vtk_grid, coordinates_array): From 1aedc78674dbbcd974043c52df631e80558a4e30 Mon Sep 17 00:00:00 2001 From: "paul.profizi" Date: Fri, 25 Oct 2024 15:29:11 +0200 Subject: [PATCH 12/21] Return validity grid Signed-off-by: paul.profizi --- src/ansys/dpf/core/vtk_helper.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/ansys/dpf/core/vtk_helper.py b/src/ansys/dpf/core/vtk_helper.py index 668b876a9c..5bd8e88c68 100644 --- a/src/ansys/dpf/core/vtk_helper.py +++ b/src/ansys/dpf/core/vtk_helper.py @@ -392,13 +392,15 @@ def dpf_mesh_to_vtk( except (AttributeError, KeyError, errors.DPFServerException): grid = dpf_mesh_to_vtk_py(mesh, nodes, as_linear) if check_validity: - valid, msg = vtk_mesh_is_valid(grid) + valid, msg, _ = vtk_mesh_is_valid(grid) if not valid: warnings.warn(f"\nVTK mesh validity check\n{msg}") return grid -def vtk_mesh_is_valid(grid: pv.UnstructuredGrid, verbose: bool = False) -> (bool, str): +def vtk_mesh_is_valid( + grid: pv.UnstructuredGrid, verbose: bool = False +) -> tuple[bool, str, pv.UnstructuredGrid]: """Runs a vtk.CellValidator filter on the input grid. Parameters @@ -414,6 +416,8 @@ def vtk_mesh_is_valid(grid: pv.UnstructuredGrid, verbose: bool = False) -> (bool Whether the vtk mesh is valid according to the vtkCellValidator. message: Output message. + validity_grid: + A copy of the original grid, with validity fields. """ from enum import Enum @@ -436,9 +440,8 @@ class State(Enum): cell_validator.SetInputData(grid) cell_validator.Update() # Get the states for all cells as a numpy array - cell_states = vtk_to_numpy( - cell_validator.GetUnstructuredGridOutput().GetCellData().GetArray("ValidityState") - ) + validity_grid = cell_validator.GetUnstructuredGridOutput() + cell_states = vtk_to_numpy(validity_grid.GetCellData().GetArray("ValidityState")) # Check for invalid states elem_with_wrong_number_of_nodes = np.where(cell_states == State.WrongNumberOfPoints.value)[0] elem_with_intersecting_edges = np.where(cell_states == State.IntersectingEdges.value)[0] @@ -487,7 +490,7 @@ class State(Enum): out_msg += f" {elem_with_badly_oriented_faces}\n" if verbose: print(out_msg) - return mesh_is_valid, out_msg + return mesh_is_valid, out_msg, pv.UnstructuredGrid(validity_grid) def vtk_update_coordinates(vtk_grid, coordinates_array): From 44bc814b43375cb66ce7bbc55be19bc94d1c53d2 Mon Sep 17 00:00:00 2001 From: "paul.profizi" Date: Fri, 25 Oct 2024 15:31:28 +0200 Subject: [PATCH 13/21] Return validity grid Signed-off-by: paul.profizi --- src/ansys/dpf/core/vtk_helper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ansys/dpf/core/vtk_helper.py b/src/ansys/dpf/core/vtk_helper.py index 5bd8e88c68..3c610265b0 100644 --- a/src/ansys/dpf/core/vtk_helper.py +++ b/src/ansys/dpf/core/vtk_helper.py @@ -490,7 +490,9 @@ class State(Enum): out_msg += f" {elem_with_badly_oriented_faces}\n" if verbose: print(out_msg) - return mesh_is_valid, out_msg, pv.UnstructuredGrid(validity_grid) + validity_grid = pv.UnstructuredGrid(validity_grid) + validity_grid.set_active_scalars("ValidityState") + return mesh_is_valid, out_msg, validity_grid def vtk_update_coordinates(vtk_grid, coordinates_array): From 73198f13387c61c0215331a745ce7a691bbc205f Mon Sep 17 00:00:00 2001 From: Sean Ahern Date: Fri, 25 Oct 2024 11:12:51 -0400 Subject: [PATCH 14/21] fix: use bitwise operations for checking validity states --- src/ansys/dpf/core/vtk_helper.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/ansys/dpf/core/vtk_helper.py b/src/ansys/dpf/core/vtk_helper.py index 3c610265b0..3d9fae1489 100644 --- a/src/ansys/dpf/core/vtk_helper.py +++ b/src/ansys/dpf/core/vtk_helper.py @@ -443,14 +443,13 @@ class State(Enum): validity_grid = cell_validator.GetUnstructuredGridOutput() cell_states = vtk_to_numpy(validity_grid.GetCellData().GetArray("ValidityState")) # Check for invalid states - elem_with_wrong_number_of_nodes = np.where(cell_states == State.WrongNumberOfPoints.value)[0] - elem_with_intersecting_edges = np.where(cell_states == State.IntersectingEdges.value)[0] - elem_with_intersecting_faces = np.where(cell_states == State.IntersectingFaces.value)[0] - elem_with_noncontiguous_edges = np.where(cell_states == State.NoncontiguousEdges.value)[0] - elem_with_nonconvex_shape = np.where(cell_states == State.Nonconvex.value)[0] - elem_with_badly_oriented_faces = np.where( - cell_states == State.FacesAreOrientedIncorrectly.value - )[0] + elem_with_wrong_number_of_nodes = np.where(cell_states & State.WrongNumberOfPoints.value)[0] + elem_with_intersecting_edges = np.where(cell_states & State.IntersectingEdges.value)[0] + elem_with_intersecting_faces = np.where(cell_states & State.IntersectingFaces.value)[0] + elem_with_noncontiguous_edges = np.where(cell_states & State.NoncontiguousEdges.value)[0] + elem_with_nonconvex_shape = np.where(cell_states & State.Nonconvex.value)[0] + elem_with_badly_oriented_faces = np.where(cell_states & State.FacesAreOrientedIncorrectly.value)[0] + # Build list of number of elements failing each test failing_elements_number = [ len(elem_with_wrong_number_of_nodes), From b536e1aabd28555fd7bd2ace82a64c1156447319 Mon Sep 17 00:00:00 2001 From: Sean Ahern Date: Fri, 25 Oct 2024 13:30:14 -0400 Subject: [PATCH 15/21] chore: Reformatted lines to better fit line length limits --- src/ansys/dpf/core/vtk_helper.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/ansys/dpf/core/vtk_helper.py b/src/ansys/dpf/core/vtk_helper.py index 3d9fae1489..7349fac268 100644 --- a/src/ansys/dpf/core/vtk_helper.py +++ b/src/ansys/dpf/core/vtk_helper.py @@ -443,12 +443,18 @@ class State(Enum): validity_grid = cell_validator.GetUnstructuredGridOutput() cell_states = vtk_to_numpy(validity_grid.GetCellData().GetArray("ValidityState")) # Check for invalid states - elem_with_wrong_number_of_nodes = np.where(cell_states & State.WrongNumberOfPoints.value)[0] - elem_with_intersecting_edges = np.where(cell_states & State.IntersectingEdges.value)[0] - elem_with_intersecting_faces = np.where(cell_states & State.IntersectingFaces.value)[0] - elem_with_noncontiguous_edges = np.where(cell_states & State.NoncontiguousEdges.value)[0] - elem_with_nonconvex_shape = np.where(cell_states & State.Nonconvex.value)[0] - elem_with_badly_oriented_faces = np.where(cell_states & State.FacesAreOrientedIncorrectly.value)[0] + elem_with_wrong_number_of_nodes = np.where( + cell_states & State.WrongNumberOfPoints.value)[0] + elem_with_intersecting_edges = np.where( + cell_states & State.IntersectingEdges.value)[0] + elem_with_intersecting_faces = np.where( + cell_states & State.IntersectingFaces.value)[0] + elem_with_noncontiguous_edges = np.where( + cell_states & State.NoncontiguousEdges.value)[0] + elem_with_nonconvex_shape = np.where( + cell_states & State.Nonconvex.value)[0] + elem_with_badly_oriented_faces = np.where( + cell_states & State.FacesAreOrientedIncorrectly.value)[0] # Build list of number of elements failing each test failing_elements_number = [ From 65324a4abc6d2e93f6cb9f6d48d9244a7d53433a Mon Sep 17 00:00:00 2001 From: "paul.profizi" Date: Mon, 28 Oct 2024 09:40:32 +0100 Subject: [PATCH 16/21] Style check Signed-off-by: paul.profizi --- src/ansys/dpf/core/vtk_helper.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/ansys/dpf/core/vtk_helper.py b/src/ansys/dpf/core/vtk_helper.py index 7349fac268..fe064fe7e2 100644 --- a/src/ansys/dpf/core/vtk_helper.py +++ b/src/ansys/dpf/core/vtk_helper.py @@ -443,18 +443,14 @@ class State(Enum): validity_grid = cell_validator.GetUnstructuredGridOutput() cell_states = vtk_to_numpy(validity_grid.GetCellData().GetArray("ValidityState")) # Check for invalid states - elem_with_wrong_number_of_nodes = np.where( - cell_states & State.WrongNumberOfPoints.value)[0] - elem_with_intersecting_edges = np.where( - cell_states & State.IntersectingEdges.value)[0] - elem_with_intersecting_faces = np.where( - cell_states & State.IntersectingFaces.value)[0] - elem_with_noncontiguous_edges = np.where( - cell_states & State.NoncontiguousEdges.value)[0] - elem_with_nonconvex_shape = np.where( - cell_states & State.Nonconvex.value)[0] + elem_with_wrong_number_of_nodes = np.where(cell_states & State.WrongNumberOfPoints.value)[0] + elem_with_intersecting_edges = np.where(cell_states & State.IntersectingEdges.value)[0] + elem_with_intersecting_faces = np.where(cell_states & State.IntersectingFaces.value)[0] + elem_with_noncontiguous_edges = np.where(cell_states & State.NoncontiguousEdges.value)[0] + elem_with_nonconvex_shape = np.where(cell_states & State.Nonconvex.value)[0] elem_with_badly_oriented_faces = np.where( - cell_states & State.FacesAreOrientedIncorrectly.value)[0] + cell_states & State.FacesAreOrientedIncorrectly.value + )[0] # Build list of number of elements failing each test failing_elements_number = [ From 3232720305c8d05e156b90db5480b86335036427 Mon Sep 17 00:00:00 2001 From: "paul.profizi" Date: Tue, 29 Oct 2024 15:00:44 +0100 Subject: [PATCH 17/21] Add testing Signed-off-by: paul.profizi --- tests/test_vtk_translate.py | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_vtk_translate.py b/tests/test_vtk_translate.py index 89e0a62277..1066bfa24c 100644 --- a/tests/test_vtk_translate.py +++ b/tests/test_vtk_translate.py @@ -32,6 +32,7 @@ dpf_property_field_to_vtk, append_field_to_grid, append_fieldscontainer_to_grid, + vtk_mesh_is_valid, ) if misc.module_exists("pyvista"): @@ -226,3 +227,44 @@ def test_append_fields_container_to_grid(simple_rst, server_type): assert isinstance(ug, pv.UnstructuredGrid) assert "disp {'time': 1}" in ug.point_data.keys() assert "volume {'time': 1}" in ug.cell_data.keys() + + +@pytest.mark.skipif(not HAS_PYVISTA, reason="Please install pyvista") +def test_vtk_mesh_is_valid_polyhedron(): + # Element type is polyhedron + cell_types = [42] + + # Start with a valid element + nodes_1 = [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 0.5], + [1.0, 0.0, 0.5], + [0.0, 1.0, 0.5], + ] + cells_1 = [24, 5, 4, 4, 1, 2, 5, 4, 3, 0, 1, 4, 3, 2, 1, 0, 3, 3, 4, 5, 4, 5, 2, 0, 3] + grid = pv.UnstructuredGrid(cells_1, cell_types, nodes_1) + valid, msg, out_grid = vtk_mesh_is_valid(grid) + assert valid + + # Move one node + nodes_2 = [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, -0.05, 0.5], # Moved one node along Y axis + [1.0, 0.0, 0.5], + [0.0, 1.0, 0.5], + ] + grid = pv.UnstructuredGrid(cells_1, cell_types, nodes_2) + valid, msg, out_grid = vtk_mesh_is_valid(grid) + print(msg) + assert not valid # For some reason this element is found to be non-convex + + # Invert one face + cells_2 = [24, 5, 4, 4, 1, 2, 5, 4, 3, 0, 1, 4, 3, 2, 1, 0, 3, 5, 4, 3, 4, 5, 2, 0, 3] + grid = pv.UnstructuredGrid(cells_2, cell_types, nodes_1) + valid, msg, out_grid = vtk_mesh_is_valid(grid) + print(msg) + assert not valid # Non-convex AND bad face orientation From c42e7163aecc87788249e4b1e3ffd1f435994203 Mon Sep 17 00:00:00 2001 From: Sean Ahern Date: Tue, 29 Oct 2024 10:45:36 -0400 Subject: [PATCH 18/21] Turn magic number into a named constant --- tests/test_vtk_translate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_vtk_translate.py b/tests/test_vtk_translate.py index 1066bfa24c..272eb9073d 100644 --- a/tests/test_vtk_translate.py +++ b/tests/test_vtk_translate.py @@ -232,7 +232,7 @@ def test_append_fields_container_to_grid(simple_rst, server_type): @pytest.mark.skipif(not HAS_PYVISTA, reason="Please install pyvista") def test_vtk_mesh_is_valid_polyhedron(): # Element type is polyhedron - cell_types = [42] + cell_types = [pv.CellType.POLYHEDRON] # Start with a valid element nodes_1 = [ From 6fef37915f095289d3a52c851ff1759619245430 Mon Sep 17 00:00:00 2001 From: "paul.profizi" Date: Wed, 30 Oct 2024 10:40:57 +0100 Subject: [PATCH 19/21] Propose new output structure for the validator Signed-off-by: paul.profizi --- src/ansys/dpf/core/vtk_helper.py | 81 +++++++++++++++++++++++--------- tests/test_vtk_translate.py | 37 +++++++++++---- 2 files changed, 88 insertions(+), 30 deletions(-) diff --git a/src/ansys/dpf/core/vtk_helper.py b/src/ansys/dpf/core/vtk_helper.py index fe064fe7e2..5147830c75 100644 --- a/src/ansys/dpf/core/vtk_helper.py +++ b/src/ansys/dpf/core/vtk_helper.py @@ -19,10 +19,9 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import warnings +from dataclasses import dataclass import numpy as np -import pyvista import pyvista as pv from typing import Union from vtk import ( @@ -45,6 +44,7 @@ VTK_WEDGE, vtkVersion, ) +import warnings import ansys.dpf.core as dpf from ansys.dpf.core import errors @@ -392,15 +392,47 @@ def dpf_mesh_to_vtk( except (AttributeError, KeyError, errors.DPFServerException): grid = dpf_mesh_to_vtk_py(mesh, nodes, as_linear) if check_validity: - valid, msg, _ = vtk_mesh_is_valid(grid) - if not valid: - warnings.warn(f"\nVTK mesh validity check\n{msg}") + validity = vtk_mesh_is_valid(grid) + if not validity.valid: + warnings.warn(f"\nVTK mesh validity check\n{validity.msg}") return grid -def vtk_mesh_is_valid( - grid: pv.UnstructuredGrid, verbose: bool = False -) -> tuple[bool, str, pv.UnstructuredGrid]: +@dataclass +class VTKMeshValidity: + """Dataclass containing the results of a call to vtk_mesh_is_valid. + valid: + Whether the vtk mesh is valid according to the vtkCellValidator. + message: + Output message. + validity_grid: + A copy of the original grid, with validity fields. + wrong_number_of_points: + List of indexes of elements with the wrong number of points. + intersecting_edges: + List of indexes of elements with intersecting edges. + intersecting_faces: + List of indexes of elements with intersecting faces. + non_contiguous_edges: + List of indexes of elements with non-contiguous edges. + non_convex: + List of indexes of elements with non-convex shape. + inverted_faces: + List of indexes of elements with inverted faces. + """ + + valid: bool + msg: str + grid: pv.UnstructuredGrid + wrong_number_of_points: np.ndarray + intersecting_edges: np.ndarray + intersecting_faces: np.ndarray + non_contiguous_edges: np.ndarray + non_convex: np.ndarray + inverted_faces: np.ndarray + + +def vtk_mesh_is_valid(grid: pv.UnstructuredGrid, verbose: bool = False) -> VTKMeshValidity: """Runs a vtk.CellValidator filter on the input grid. Parameters @@ -412,13 +444,8 @@ def vtk_mesh_is_valid( Returns ------- - valid: - Whether the vtk mesh is valid according to the vtkCellValidator. - message: - Output message. - validity_grid: - A copy of the original grid, with validity fields. - + validity: + A dataclass containing the results of the validator. """ from enum import Enum @@ -446,8 +473,8 @@ class State(Enum): elem_with_wrong_number_of_nodes = np.where(cell_states & State.WrongNumberOfPoints.value)[0] elem_with_intersecting_edges = np.where(cell_states & State.IntersectingEdges.value)[0] elem_with_intersecting_faces = np.where(cell_states & State.IntersectingFaces.value)[0] - elem_with_noncontiguous_edges = np.where(cell_states & State.NoncontiguousEdges.value)[0] - elem_with_nonconvex_shape = np.where(cell_states & State.Nonconvex.value)[0] + elem_with_non_contiguous_edges = np.where(cell_states & State.NoncontiguousEdges.value)[0] + elem_with_non_convex_shape = np.where(cell_states & State.Nonconvex.value)[0] elem_with_badly_oriented_faces = np.where( cell_states & State.FacesAreOrientedIncorrectly.value )[0] @@ -457,8 +484,8 @@ class State(Enum): len(elem_with_wrong_number_of_nodes), len(elem_with_intersecting_edges), len(elem_with_intersecting_faces), - len(elem_with_noncontiguous_edges), - len(elem_with_nonconvex_shape), + len(elem_with_non_contiguous_edges), + len(elem_with_non_convex_shape), len(elem_with_badly_oriented_faces), ] # Define whether mesh is valid @@ -482,10 +509,10 @@ class State(Enum): out_msg += f" {elem_with_intersecting_faces}\n" if failing_elements_number[3] > 0: out_msg += f" - {failing_elements_number[3]} elements with non contiguous edges:\n" - out_msg += f" {elem_with_noncontiguous_edges}\n" + out_msg += f" {elem_with_non_contiguous_edges}\n" if failing_elements_number[4] > 0: out_msg += f" - {failing_elements_number[4]} elements with non convex shape:\n" - out_msg += f" {elem_with_nonconvex_shape}\n" + out_msg += f" {elem_with_non_convex_shape}\n" if failing_elements_number[5] > 0: out_msg += f" - {failing_elements_number[5]} elements with bad face orientations:\n" out_msg += f" {elem_with_badly_oriented_faces}\n" @@ -493,7 +520,17 @@ class State(Enum): print(out_msg) validity_grid = pv.UnstructuredGrid(validity_grid) validity_grid.set_active_scalars("ValidityState") - return mesh_is_valid, out_msg, validity_grid + return VTKMeshValidity( + valid=mesh_is_valid, + msg=out_msg, + grid=validity_grid, + wrong_number_of_points=elem_with_wrong_number_of_nodes, + intersecting_edges=elem_with_intersecting_edges, + intersecting_faces=elem_with_intersecting_faces, + non_contiguous_edges=elem_with_non_contiguous_edges, + non_convex=elem_with_non_convex_shape, + inverted_faces=elem_with_badly_oriented_faces, + ) def vtk_update_coordinates(vtk_grid, coordinates_array): diff --git a/tests/test_vtk_translate.py b/tests/test_vtk_translate.py index 272eb9073d..a2dea39af6 100644 --- a/tests/test_vtk_translate.py +++ b/tests/test_vtk_translate.py @@ -245,8 +245,17 @@ def test_vtk_mesh_is_valid_polyhedron(): ] cells_1 = [24, 5, 4, 4, 1, 2, 5, 4, 3, 0, 1, 4, 3, 2, 1, 0, 3, 3, 4, 5, 4, 5, 2, 0, 3] grid = pv.UnstructuredGrid(cells_1, cell_types, nodes_1) - valid, msg, out_grid = vtk_mesh_is_valid(grid) - assert valid + validity = vtk_mesh_is_valid(grid) + print(validity) + assert validity.valid + assert "valid" in validity.msg + assert validity.grid.active_scalars_name == "ValidityState" + assert len(validity.wrong_number_of_points) == 0 + assert len(validity.intersecting_edges) == 0 + assert len(validity.intersecting_faces) == 0 + assert len(validity.non_contiguous_edges) == 0 + assert len(validity.non_convex) == 0 + assert len(validity.inverted_faces) == 0 # Move one node nodes_2 = [ @@ -258,13 +267,25 @@ def test_vtk_mesh_is_valid_polyhedron(): [0.0, 1.0, 0.5], ] grid = pv.UnstructuredGrid(cells_1, cell_types, nodes_2) - valid, msg, out_grid = vtk_mesh_is_valid(grid) - print(msg) - assert not valid # For some reason this element is found to be non-convex + validity = vtk_mesh_is_valid(grid) + print(validity) + assert not validity.valid # For some reason this element is found to be non-convex + assert len(validity.wrong_number_of_points) == 0 + assert len(validity.intersecting_edges) == 0 + assert len(validity.intersecting_faces) == 0 + assert len(validity.non_contiguous_edges) == 0 + assert len(validity.non_convex) == 1 + assert len(validity.inverted_faces) == 0 # Invert one face cells_2 = [24, 5, 4, 4, 1, 2, 5, 4, 3, 0, 1, 4, 3, 2, 1, 0, 3, 5, 4, 3, 4, 5, 2, 0, 3] grid = pv.UnstructuredGrid(cells_2, cell_types, nodes_1) - valid, msg, out_grid = vtk_mesh_is_valid(grid) - print(msg) - assert not valid # Non-convex AND bad face orientation + validity = vtk_mesh_is_valid(grid) + print(validity) + assert not validity.valid # Non-convex AND bad face orientation + assert len(validity.wrong_number_of_points) == 0 + assert len(validity.intersecting_edges) == 0 + assert len(validity.intersecting_faces) == 0 + assert len(validity.non_contiguous_edges) == 0 + assert len(validity.non_convex) == 1 + assert len(validity.inverted_faces) == 1 From af8aa2b9d5d5696794cceee94a92111e7cb4f35b Mon Sep 17 00:00:00 2001 From: Sean Ahern Date: Mon, 4 Nov 2024 09:03:25 -0500 Subject: [PATCH 20/21] Refactor connectivity for vtk mesh validity to improve readability --- tests/test_vtk_translate.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/tests/test_vtk_translate.py b/tests/test_vtk_translate.py index a2dea39af6..24baf22354 100644 --- a/tests/test_vtk_translate.py +++ b/tests/test_vtk_translate.py @@ -243,8 +243,19 @@ def test_vtk_mesh_is_valid_polyhedron(): [1.0, 0.0, 0.5], [0.0, 1.0, 0.5], ] - cells_1 = [24, 5, 4, 4, 1, 2, 5, 4, 3, 0, 1, 4, 3, 2, 1, 0, 3, 3, 4, 5, 4, 5, 2, 0, 3] - grid = pv.UnstructuredGrid(cells_1, cell_types, nodes_1) + cells_1 = [ + 5, + 4, 4, 1, 2, 5, + 4, + 3, 0, 1, 4, + 3, + 2, 1, 0, + 3, + 3, 4, 5, + 4, + 5, 2, 0, 3 + ] + grid = pv.UnstructuredGrid([len(cells_1), *cells_1], cell_types, nodes_1) validity = vtk_mesh_is_valid(grid) print(validity) assert validity.valid @@ -278,8 +289,19 @@ def test_vtk_mesh_is_valid_polyhedron(): assert len(validity.inverted_faces) == 0 # Invert one face - cells_2 = [24, 5, 4, 4, 1, 2, 5, 4, 3, 0, 1, 4, 3, 2, 1, 0, 3, 5, 4, 3, 4, 5, 2, 0, 3] - grid = pv.UnstructuredGrid(cells_2, cell_types, nodes_1) + cells_2 = [ + 5, + 4, 4, 1, 2, 5, + 4, + 3, 0, 1, 4, + 3, + 2, 1, 0, + 3, + 5, 4, 3, # Inverted face + 4, + 5, 2, 0, 3 + ] + grid = pv.UnstructuredGrid([len(cells_2), *cells_2], cell_types, nodes_1) validity = vtk_mesh_is_valid(grid) print(validity) assert not validity.valid # Non-convex AND bad face orientation From 5aa311e54df347ad37aa06b34984669a5f0c0303 Mon Sep 17 00:00:00 2001 From: PProfizi Date: Mon, 5 May 2025 10:25:03 +0200 Subject: [PATCH 21/21] Fix test_vtk_mesh_is_valid_polyhedron --- tests/test_vtk_translate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_vtk_translate.py b/tests/test_vtk_translate.py index f49d8edc90..795e9a2cbe 100644 --- a/tests/test_vtk_translate.py +++ b/tests/test_vtk_translate.py @@ -270,7 +270,7 @@ def test_vtk_mesh_is_valid_polyhedron(): [1.0, 0.0, 0.5], [0.0, 1.0, 0.5], ] - grid = pv.UnstructuredGrid(cells_1, cell_types, nodes_2) + grid = pv.UnstructuredGrid([len(cells_1), *cells_1], cell_types, nodes_2) validity = vtk_mesh_is_valid(grid) print(validity) assert not validity.valid # For some reason this element is found to be non-convex