Skip to content

Commit c4298ea

Browse files
PProfiziahernsean
andauthored
Add a vtk_mesh_is_valid helper to check validity of VTK meshes (#1836)
* 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 <[email protected]> * Return validity grid Signed-off-by: paul.profizi <[email protected]> * Return validity grid Signed-off-by: paul.profizi <[email protected]> * fix: use bitwise operations for checking validity states * chore: Reformatted lines to better fit line length limits * Style check Signed-off-by: paul.profizi <[email protected]> * Add testing Signed-off-by: paul.profizi <[email protected]> * Turn magic number into a named constant * Propose new output structure for the validator Signed-off-by: paul.profizi <[email protected]> * Refactor connectivity for vtk mesh validity to improve readability * 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 <[email protected]> * Return validity grid Signed-off-by: paul.profizi <[email protected]> * Return validity grid Signed-off-by: paul.profizi <[email protected]> * fix: use bitwise operations for checking validity states * chore: Reformatted lines to better fit line length limits * Style check Signed-off-by: paul.profizi <[email protected]> * Add testing Signed-off-by: paul.profizi <[email protected]> * Turn magic number into a named constant * Propose new output structure for the validator Signed-off-by: paul.profizi <[email protected]> * Refactor connectivity for vtk mesh validity to improve readability * Fix test_vtk_mesh_is_valid_polyhedron --------- Signed-off-by: paul.profizi <[email protected]> Co-authored-by: Sean Ahern <[email protected]>
1 parent 5efdcaf commit c4298ea

File tree

2 files changed

+236
-4
lines changed

2 files changed

+236
-4
lines changed

src/ansys/dpf/core/vtk_helper.py

Lines changed: 148 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222

2323
"""Provides for vtk helper functions."""
2424

25+
from dataclasses import dataclass
2526
from typing import Union
27+
import warnings
2628

2729
import numpy as np
2830
import pyvista as pv
@@ -363,6 +365,7 @@ def dpf_mesh_to_vtk(
363365
mesh: dpf.MeshedRegion,
364366
nodes: Union[dpf.Field, None] = None,
365367
as_linear: bool = True,
368+
check_validity: bool = False,
366369
) -> pv.UnstructuredGrid:
367370
"""Return a pyvista UnstructuredGrid given a pydpf MeshedRegion.
368371
@@ -377,18 +380,159 @@ def dpf_mesh_to_vtk(
377380
as_linear:
378381
Export quadratic surface elements as linear.
379382
380-
export_faces:
381-
Whether to export face elements along with volume elements for fluid meshes.
383+
check_validity:
384+
Whether to run the VTK cell validity check on the generated mesh and warn if not valid.
382385
383386
Returns
384387
-------
385388
grid:
386389
UnstructuredGrid corresponding to the DPF mesh.
387390
"""
388391
try:
389-
return dpf_mesh_to_vtk_op(mesh, nodes, as_linear)
392+
grid = dpf_mesh_to_vtk_op(mesh, nodes, as_linear)
390393
except (AttributeError, KeyError, errors.DPFServerException):
391-
return dpf_mesh_to_vtk_py(mesh, nodes, as_linear)
394+
grid = dpf_mesh_to_vtk_py(mesh, nodes, as_linear)
395+
if check_validity:
396+
validity = vtk_mesh_is_valid(grid)
397+
if not validity.valid:
398+
warnings.warn(f"\nVTK mesh validity check\n{validity.msg}")
399+
return grid
400+
401+
402+
@dataclass
403+
class VTKMeshValidity:
404+
"""Dataclass containing the results of a call to vtk_mesh_is_valid.
405+
406+
valid:
407+
Whether the vtk mesh is valid according to the vtkCellValidator.
408+
message:
409+
Output message.
410+
validity_grid:
411+
A copy of the original grid, with validity fields.
412+
wrong_number_of_points:
413+
List of indexes of elements with the wrong number of points.
414+
intersecting_edges:
415+
List of indexes of elements with intersecting edges.
416+
intersecting_faces:
417+
List of indexes of elements with intersecting faces.
418+
non_contiguous_edges:
419+
List of indexes of elements with non-contiguous edges.
420+
non_convex:
421+
List of indexes of elements with non-convex shape.
422+
inverted_faces:
423+
List of indexes of elements with inverted faces.
424+
"""
425+
426+
valid: bool
427+
msg: str
428+
grid: pv.UnstructuredGrid
429+
wrong_number_of_points: np.ndarray
430+
intersecting_edges: np.ndarray
431+
intersecting_faces: np.ndarray
432+
non_contiguous_edges: np.ndarray
433+
non_convex: np.ndarray
434+
inverted_faces: np.ndarray
435+
436+
437+
def vtk_mesh_is_valid(grid: pv.UnstructuredGrid, verbose: bool = False) -> VTKMeshValidity:
438+
"""Run a vtk.CellValidator filter on the input grid.
439+
440+
Parameters
441+
----------
442+
grid:
443+
A vtk mesh to validate.
444+
verbose:
445+
Whether to print the complete validation.
446+
447+
Returns
448+
-------
449+
validity:
450+
A dataclass containing the results of the validator.
451+
"""
452+
from enum import Enum
453+
454+
from vtkmodules.util.numpy_support import vtk_to_numpy
455+
from vtkmodules.vtkFiltersGeneral import vtkCellValidator
456+
457+
# Prepare the Enum of possible validity states
458+
class State(Enum):
459+
Valid = 0
460+
WrongNumberOfPoints = (1,)
461+
IntersectingEdges = (2,)
462+
IntersectingFaces = (4,)
463+
NoncontiguousEdges = (8,)
464+
Nonconvex = (16,)
465+
FacesAreOrientedIncorrectly = (32,)
466+
467+
# Run the cell validator
468+
cell_validator = vtkCellValidator()
469+
cell_validator.SetInputData(grid)
470+
cell_validator.Update()
471+
# Get the states for all cells as a numpy array
472+
validity_grid = cell_validator.GetUnstructuredGridOutput()
473+
cell_states = vtk_to_numpy(validity_grid.GetCellData().GetArray("ValidityState"))
474+
# Check for invalid states
475+
elem_with_wrong_number_of_nodes = np.where(cell_states & State.WrongNumberOfPoints.value)[0]
476+
elem_with_intersecting_edges = np.where(cell_states & State.IntersectingEdges.value)[0]
477+
elem_with_intersecting_faces = np.where(cell_states & State.IntersectingFaces.value)[0]
478+
elem_with_non_contiguous_edges = np.where(cell_states & State.NoncontiguousEdges.value)[0]
479+
elem_with_non_convex_shape = np.where(cell_states & State.Nonconvex.value)[0]
480+
elem_with_badly_oriented_faces = np.where(
481+
cell_states & State.FacesAreOrientedIncorrectly.value
482+
)[0]
483+
484+
# Build list of number of elements failing each test
485+
failing_elements_number = [
486+
len(elem_with_wrong_number_of_nodes),
487+
len(elem_with_intersecting_edges),
488+
len(elem_with_intersecting_faces),
489+
len(elem_with_non_contiguous_edges),
490+
len(elem_with_non_convex_shape),
491+
len(elem_with_badly_oriented_faces),
492+
]
493+
# Define whether mesh is valid
494+
mesh_is_valid = np.sum(failing_elements_number) == 0
495+
# Build output message
496+
out_msg = ""
497+
if mesh_is_valid:
498+
out_msg += "Mesh is valid."
499+
else:
500+
out_msg += "Mesh is invalid because of (by index):\n"
501+
if failing_elements_number[0] > 0:
502+
out_msg += (
503+
f" - {failing_elements_number[0]} elements with the wrong number of points:\n"
504+
)
505+
out_msg += f" {elem_with_wrong_number_of_nodes}\n"
506+
if failing_elements_number[1] > 0:
507+
out_msg += f" - {failing_elements_number[1]} elements with intersecting edges:\n"
508+
out_msg += f" {elem_with_intersecting_edges}\n"
509+
if failing_elements_number[2] > 0:
510+
out_msg += f" - {failing_elements_number[2]} elements with intersecting faces:\n"
511+
out_msg += f" {elem_with_intersecting_faces}\n"
512+
if failing_elements_number[3] > 0:
513+
out_msg += f" - {failing_elements_number[3]} elements with non contiguous edges:\n"
514+
out_msg += f" {elem_with_non_contiguous_edges}\n"
515+
if failing_elements_number[4] > 0:
516+
out_msg += f" - {failing_elements_number[4]} elements with non convex shape:\n"
517+
out_msg += f" {elem_with_non_convex_shape}\n"
518+
if failing_elements_number[5] > 0:
519+
out_msg += f" - {failing_elements_number[5]} elements with bad face orientations:\n"
520+
out_msg += f" {elem_with_badly_oriented_faces}\n"
521+
if verbose:
522+
print(out_msg)
523+
validity_grid = pv.UnstructuredGrid(validity_grid)
524+
validity_grid.set_active_scalars("ValidityState")
525+
return VTKMeshValidity(
526+
valid=mesh_is_valid,
527+
msg=out_msg,
528+
grid=validity_grid,
529+
wrong_number_of_points=elem_with_wrong_number_of_nodes,
530+
intersecting_edges=elem_with_intersecting_edges,
531+
intersecting_faces=elem_with_intersecting_faces,
532+
non_contiguous_edges=elem_with_non_contiguous_edges,
533+
non_convex=elem_with_non_convex_shape,
534+
inverted_faces=elem_with_badly_oriented_faces,
535+
)
392536

393537

394538
def vtk_update_coordinates(vtk_grid, coordinates_array):

tests/test_vtk_translate.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
dpf_mesh_to_vtk,
3333
dpf_meshes_to_vtk,
3434
dpf_property_field_to_vtk,
35+
vtk_mesh_is_valid,
3536
)
3637
import conftest
3738

@@ -230,3 +231,90 @@ def test_append_fields_container_to_grid(simple_rst, server_type):
230231
assert isinstance(ug, pv.UnstructuredGrid)
231232
assert "disp {'time': 1}" in ug.point_data.keys()
232233
assert "volume {'time': 1}" in ug.cell_data.keys()
234+
235+
236+
@pytest.mark.skipif(not HAS_PYVISTA, reason="Please install pyvista")
237+
def test_vtk_mesh_is_valid_polyhedron():
238+
# Element type is polyhedron
239+
cell_types = [pv.CellType.POLYHEDRON]
240+
241+
# Start with a valid element
242+
nodes_1 = [
243+
[0.0, 0.0, 0.0],
244+
[1.0, 0.0, 0.0],
245+
[0.0, 1.0, 0.0],
246+
[0.0, 0.0, 0.5],
247+
[1.0, 0.0, 0.5],
248+
[0.0, 1.0, 0.5],
249+
]
250+
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]
251+
grid = pv.UnstructuredGrid([len(cells_1), *cells_1], cell_types, nodes_1)
252+
validity = vtk_mesh_is_valid(grid)
253+
print(validity)
254+
assert validity.valid
255+
assert "valid" in validity.msg
256+
assert validity.grid.active_scalars_name == "ValidityState"
257+
assert len(validity.wrong_number_of_points) == 0
258+
assert len(validity.intersecting_edges) == 0
259+
assert len(validity.intersecting_faces) == 0
260+
assert len(validity.non_contiguous_edges) == 0
261+
assert len(validity.non_convex) == 0
262+
assert len(validity.inverted_faces) == 0
263+
264+
# Move one node
265+
nodes_2 = [
266+
[0.0, 0.0, 0.0],
267+
[1.0, 0.0, 0.0],
268+
[0.0, 1.0, 0.0],
269+
[0.0, -0.05, 0.5], # Moved one node along Y axis
270+
[1.0, 0.0, 0.5],
271+
[0.0, 1.0, 0.5],
272+
]
273+
grid = pv.UnstructuredGrid([len(cells_1), *cells_1], cell_types, nodes_2)
274+
validity = vtk_mesh_is_valid(grid)
275+
print(validity)
276+
assert not validity.valid # For some reason this element is found to be non-convex
277+
assert len(validity.wrong_number_of_points) == 0
278+
assert len(validity.intersecting_edges) == 0
279+
assert len(validity.intersecting_faces) == 0
280+
assert len(validity.non_contiguous_edges) == 0
281+
assert len(validity.non_convex) == 1
282+
assert len(validity.inverted_faces) == 0
283+
284+
# Invert one face
285+
cells_2 = [
286+
5,
287+
4,
288+
4,
289+
1,
290+
2,
291+
5,
292+
4,
293+
3,
294+
0,
295+
1,
296+
4,
297+
3,
298+
2,
299+
1,
300+
0,
301+
3,
302+
5,
303+
4,
304+
3, # Inverted face
305+
4,
306+
5,
307+
2,
308+
0,
309+
3,
310+
]
311+
grid = pv.UnstructuredGrid([len(cells_2), *cells_2], cell_types, nodes_1)
312+
validity = vtk_mesh_is_valid(grid)
313+
print(validity)
314+
assert not validity.valid # Non-convex AND bad face orientation
315+
assert len(validity.wrong_number_of_points) == 0
316+
assert len(validity.intersecting_edges) == 0
317+
assert len(validity.intersecting_faces) == 0
318+
assert len(validity.non_contiguous_edges) == 0
319+
assert len(validity.non_convex) == 1
320+
assert len(validity.inverted_faces) == 1

0 commit comments

Comments
 (0)