Mesh Validation#

This example explores different cases where a mesh may not be considered valid as defined by the validate_mesh() method.

from __future__ import annotations

import pyvista as pv
from pyvista.examples import plot_cell

Non-convex cells#

Many VTK algorithms assume that cells are convex. This can result in incorrect outputs and may also affect rendering. For example, let’s create PolyData with a concave QUAD cell.

points = [
    [-0.5, -1.0, 0.0],
    [0.0, -0.3, 0.0],
    [1.0, 0.0, 0.0],
    [-0.5, 0.0, 0.0],
]
faces = [4, 0, 1, 2, 3]
quad = pv.PolyData(points, faces)

Use validate_mesh() to show that the cell is not convex.

report = quad.validate_mesh()
assert not report.is_valid
assert report.invalid_fields == ('non_convex',)

If we plot the cell, we can see that the concave cell is incorrectly rendered as though it’s convex even though it is not.

plot_cell(quad, 'xy')
mesh validation

To address the convexity problem, we can triangulate() the mesh. The mesh is now valid and renders correctly.

triangles = quad.triangulate()
report = triangles.validate_mesh()
assert report.is_valid
plot_cell(triangles, 'xy')
mesh validation

Cells with inverted faces#

Cells with inverted faces can result in incorrect geometric computations such as cell volume or centroid. To demonstrate this, we first create a valid POLYHEDRON cell similar to the Polyhedron() example cell.

points = [[0, 0, 0], [1, 0, 0], [0.5, 0.5, 0], [0, 0, 1]]
cells = [4, 3, 0, 2, 1, 3, 0, 1, 3, 3, 0, 3, 2, 3, 1, 2, 3]
cells = [len(cells), *cells.copy()]
polyhedron = pv.UnstructuredGrid(cells, [pv.CellType.POLYHEDRON], points)

Plot the cell and show its normals. Since all points have counter-clockwise traversal, the normals all point outward and the cell is valid.

report = polyhedron.validate_mesh()
assert report.is_valid
plot_cell(polyhedron, show_normals=True)
mesh validation

Now swap two points in the polyhedron’s connectivity to generate an otherwise identical polyhedron with a single incorrectly oriented face.

index1 = 3  # index of first point ID of first face
index2 = index1 + 1  # index of second point ID of first face
point_id1 = cells[index1]
cells[index1] = cells[index2]
cells[index2] = point_id1

invalid_polyhedron = pv.UnstructuredGrid(cells, [pv.CellType.POLYHEDRON], points)

The cell is now invalid, and the bottom face is incorrectly oriented with its normal pointing inward.

report = invalid_polyhedron.validate_mesh()
assert not report.is_valid
plot_cell(invalid_polyhedron, show_normals=True)
mesh validation

If we review the invalid fields, we see that two are reported instead of only one.

assert report.invalid_fields == ('non_convex', 'inverted_faces')

The 'inverted_faces' issue is accurate, but the 'non_convex' issue is a false-positive, since the only real problem is with the face orientation. But since the face orientation is wrong, it’s no longer possible for the mesh validation to accurately determine the cell convexity. This can sometimes make identifying the core issue with a cell challenging.

Now let’s compare the centroid of the valid and invalid cells using cell_centers(). The computed centroids differ, demonstrating the need to have valid cells when using filters that depend on geometric properties.

[0.37499999999999994, 0.12499999999999997, 0.24999999999999994]
[0.28125000000000006, 0.09375, 0.43749999999999994]

Self-intersecting cells#

Most cell types have a defined point order which must be respected. For example, let’s try to create a HEXAHEDRON cell with eight points:

points = [
    [0.0, 0.0, 0.0],
    [1.0, 0.0, 0.0],
    [0.0, 1.0, 0.0],
    [0.0, 0.0, 1.0],
    [1.0, 1.0, 0.0],
    [1.0, 0.0, 1.0],
    [0.0, 1.0, 1.0],
    [1.0, 1.0, 1.0],
]
cells = [8, 0, 1, 2, 3, 4, 5, 6, 7]
celltype = [pv.CellType.HEXAHEDRON]
hexahedron = pv.UnstructuredGrid(cells, celltype, points)

At a quick glance, the cell may appear to be valid, but it is not, since the point ordering is incorrect.

report = hexahedron.validate_mesh()
assert not report.is_valid
plot_cell(hexahedron)
mesh validation

Let’s review the invalid fields reported.

assert report.invalid_fields == ('intersecting_edges', 'non_convex', 'inverted_faces')

Similar to the cell from Cells with inverted faces, multiple invalid fields are reported. From the plot above, we can see the 'intersecting_edges' issue appears to be correct, but to investigate the 'inverted_faces' problem further, let’s plot the cell again with normals.

plot_cell(hexahedron, show_normals=True)
mesh validation

Since we can see some of the normals are indeed pointing inward, this confirms that both invalid reported are correct. To rectify this problem, we need to re-order the cell connectivity based on the required ordering stated in the documentation for vtkHexahedron.

cells = [8, 0, 1, 4, 2, 3, 5, 7, 6]  # instead of [8, 0, 1, 2, 3, 4, 5, 6, 7]
celltype = [pv.CellType.HEXAHEDRON]
hexahedron = pv.UnstructuredGrid(cells, celltype, points)
report = hexahedron.validate_mesh()
assert report.is_valid
plot_cell(hexahedron)
mesh validation

Meshes with unused points#

Unused points are points not associated with any cells. These points are not processed consistently by filters and are often ignored or removed. To demonstrate this, create an UnstructuredGrid with a single unused point.

grid = pv.UnstructuredGrid()
grid.points = [[0.0, 0.0, 0.0]]
assert grid.n_points == 1
assert grid.n_cells == 0

This mesh is not considered valid.

report = grid.validate_mesh()
assert not report.is_valid
assert report.invalid_fields == ('unused_points',)

Use extract_geometry() on the grid and observe that the unused point is removed.

To remedy this, it is recommended to always associate individual points with a VERTEX cell. E.g.:

points = [[0.0, 0.0, 0.0]]
cells = [1, 0]
celltypes = [pv.CellType.VERTEX]
grid = pv.UnstructuredGrid(cells, celltypes, points)
assert grid.n_points == 1
assert grid.n_cells == 1

This time, the point is properly processed by the filter and is retained.

This mesh is also now considered valid.

report = grid.validate_mesh()
assert report.is_valid
assert not report.invalid_fields

Total running time of the script: (0 minutes 1.000 seconds)

Gallery generated by Sphinx-Gallery