Skip to content

Commit 0535dff

Browse files
feat: Add mesh tetrahedra analysis filter and plugin (#218)
* make filter class from existing script * Adding docstring * Add corresponding plugin * Adding a test * Modifs from review comments
1 parent 6a2c9ae commit 0535dff

File tree

7 files changed

+7999
-0
lines changed

7 files changed

+7999
-0
lines changed
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
3+
# SPDX-FileContributor: Bertrand Denel, Paloma Martinez
4+
import numpy as np
5+
import numpy.typing as npt
6+
from typing_extensions import Any
7+
8+
from vtkmodules.vtkCommonDataModel import vtkDataSet, VTK_TETRA
9+
10+
11+
def getCoordinatesDoublePrecision( mesh: vtkDataSet ) -> npt.NDArray[ np.float64 ]:
12+
"""Get coordinates with double precision.
13+
14+
Args:
15+
mesh (vtkDataSet): Input mesh.
16+
17+
Returns:
18+
npt.NDArray[np.float64]: Coordinates
19+
"""
20+
points = mesh.GetPoints()
21+
nPoints = points.GetNumberOfPoints()
22+
23+
coords = np.zeros( ( nPoints, 3 ), dtype=np.float64 )
24+
for i in range( nPoints ):
25+
point = points.GetPoint( i )
26+
coords[ i ] = [ point[ 0 ], point[ 1 ], point[ 2 ] ]
27+
28+
return coords
29+
30+
31+
def extractTetConnectivity( mesh: vtkDataSet ) -> tuple[ npt.NDArray[ np.float64 ], npt.NDArray[ np.float64 ] ]:
32+
"""Extract connectivity for all tetrahedra.
33+
34+
Args:
35+
mesh (vtkDataSet): Mesh to analyze.
36+
37+
Returns:
38+
tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
39+
Cell IDS corresponding to tetrahedra,
40+
Connectivity of these cells
41+
"""
42+
ncells = mesh.GetNumberOfCells()
43+
tetrahedraIds = []
44+
tetrahedraConnectivity = []
45+
46+
for cellID in range( ncells ):
47+
if mesh.GetCellType( cellID ) == VTK_TETRA:
48+
cell = mesh.GetCell( cellID )
49+
pointIds = cell.GetPointIds()
50+
conn = [ pointIds.GetId( i ) for i in range( 4 ) ]
51+
tetrahedraIds.append( cellID )
52+
tetrahedraConnectivity.append( conn )
53+
54+
return np.array( tetrahedraIds ), np.array( tetrahedraConnectivity )
55+
56+
57+
def analyzeAllTets( coords: npt.NDArray[ np.float64 ],
58+
connectivity: npt.NDArray[ np.float64 ] ) -> dict[ str, dict[ str, Any ] ]:
59+
"""Vectorized analysis of all tetrahedra.
60+
61+
This analysis computes the following metrics: volumes, aspect ratio, radius ratio, flatness ratio,shape quality, min and max edge, min and max dihedral angles, dihedral range.
62+
63+
64+
Args:
65+
coords (npt.NDArray[np.float64]): Tetrahedra coordinates.
66+
connectivity (npt.NDArray[np.float64]): Connectivity.
67+
68+
Returns:
69+
dict[str, dict[str, Any]]: Dictionary with keys 'volumes', 'aspectRatio', 'radiusRatio', 'flatnessRatio', 'shapeQuality', 'minEdge', 'maxEdge', 'minDihedral', 'maxDihedral', 'dihedralRange'
70+
"""
71+
# Get coordinates for all vertices
72+
v0 = coords[ connectivity[ :, 0 ] ]
73+
v1 = coords[ connectivity[ :, 1 ] ]
74+
v2 = coords[ connectivity[ :, 2 ] ]
75+
v3 = coords[ connectivity[ :, 3 ] ]
76+
77+
# Compute edges
78+
e01 = v1 - v0
79+
e02 = v2 - v0
80+
e03 = v3 - v0
81+
e12 = v2 - v1
82+
e13 = v3 - v1
83+
e23 = v3 - v2
84+
85+
# Edge lengths
86+
l01 = np.linalg.norm( e01, axis=1 )
87+
l02 = np.linalg.norm( e02, axis=1 )
88+
l03 = np.linalg.norm( e03, axis=1 )
89+
l12 = np.linalg.norm( e12, axis=1 )
90+
l13 = np.linalg.norm( e13, axis=1 )
91+
l23 = np.linalg.norm( e23, axis=1 )
92+
93+
allEdges = np.stack( [ l01, l02, l03, l12, l13, l23 ], axis=1 )
94+
minEdge = np.min( allEdges, axis=1 )
95+
maxEdge = np.max( allEdges, axis=1 )
96+
97+
# Volumes
98+
cross_product = np.cross( e01, e02 )
99+
volumes = np.abs( np.sum( cross_product * e03, axis=1 ) / 6.0 )
100+
101+
# Face areas
102+
face012 = 0.5 * np.linalg.norm( np.cross( e01, e02 ), axis=1 )
103+
face013 = 0.5 * np.linalg.norm( np.cross( e01, e03 ), axis=1 )
104+
face023 = 0.5 * np.linalg.norm( np.cross( e02, e03 ), axis=1 )
105+
face123 = 0.5 * np.linalg.norm( np.cross( e12, e13 ), axis=1 )
106+
107+
allFaces = np.stack( [ face012, face013, face023, face123 ], axis=1 )
108+
maxFaceArea = np.max( allFaces, axis=1 )
109+
totalSurfaceArea = np.sum( allFaces, axis=1 )
110+
111+
# Aspect ratio
112+
minAltitude = 3.0 * volumes / np.maximum( maxFaceArea, 1e-15 )
113+
aspectRatio = maxEdge / np.maximum( minAltitude, 1e-15 )
114+
aspectRatio[ volumes < 1e-15 ] = np.inf
115+
116+
# Radius ratio
117+
inradius = 3.0 * volumes / np.maximum( totalSurfaceArea, 1e-15 )
118+
circumradius = ( maxEdge**3 ) / np.maximum( 24.0 * volumes, 1e-15 )
119+
radiusRatio = circumradius / np.maximum( inradius, 1e-15 )
120+
121+
# Flatness ratio
122+
flatnessRatio = volumes / np.maximum( maxFaceArea * minEdge, 1e-15 )
123+
124+
# Shape quality
125+
sumEdgeShapeQuality = np.sum( allEdges**2, axis=1 )
126+
shapeQuality = 12.0 * ( 3.0 * volumes )**( 2.0 / 3.0 ) / np.maximum( sumEdgeShapeQuality, 1e-15 )
127+
shapeQuality[ volumes < 1e-15 ] = 0.0
128+
129+
# Dihedral angles
130+
def computeDihedralAngle( normal1: npt.NDArray[ np.float64 ],
131+
normal2: npt.NDArray[ np.float64 ] ) -> npt.NDArray[ np.float64 ]:
132+
"""Compute dihedral angle between two face normals.
133+
134+
Args:
135+
normal1 (npt.NDArray[np.float64]): Normal vector 1
136+
normal2 (npt.NDArray[np.float64]): Normal vector 2
137+
138+
Returns:
139+
npt.NDArray[ np.float64 ]: Dihedral angle
140+
"""
141+
n1Norm = normal1 / np.maximum( np.linalg.norm( normal1, axis=1, keepdims=True ), 1e-15 )
142+
n2Norm = normal2 / np.maximum( np.linalg.norm( normal2, axis=1, keepdims=True ), 1e-15 )
143+
cosAngle = np.sum( n1Norm * n2Norm, axis=1 )
144+
cosAngle = np.clip( cosAngle, -1.0, 1.0 )
145+
angle = np.arccos( cosAngle ) * 180.0 / np.pi
146+
return angle
147+
148+
# Face normals
149+
normal012: npt.NDArray[ np.float64 ] = np.cross( e01, e02 )
150+
normal013: npt.NDArray[ np.float64 ] = np.cross( e01, e03 )
151+
normal023: npt.NDArray[ np.float64 ] = np.cross( e02, e03 )
152+
normal123: npt.NDArray[ np.float64 ] = np.cross( e12, e13 )
153+
154+
# Dihedral angles for each edge
155+
dihedral01 = computeDihedralAngle( normal012, normal013 )
156+
dihedral02 = computeDihedralAngle( normal012, normal023 )
157+
dihedral03 = computeDihedralAngle( normal013, normal023 )
158+
dihedral12 = computeDihedralAngle( normal012, normal123 )
159+
dihedral13 = computeDihedralAngle( normal013, normal123 )
160+
dihedral23 = computeDihedralAngle( normal023, normal123 )
161+
162+
allDihedrals = np.stack( [ dihedral01, dihedral02, dihedral03, dihedral12, dihedral13, dihedral23 ], axis=1 )
163+
164+
minDihedral = np.min( allDihedrals, axis=1 )
165+
maxDihedral = np.max( allDihedrals, axis=1 )
166+
dihedralRange = maxDihedral - minDihedral
167+
168+
return {
169+
'volumes': volumes,
170+
'aspectRatio': aspectRatio,
171+
'radiusRatio': radiusRatio,
172+
'flatnessRatio': flatnessRatio,
173+
'shapeQuality': shapeQuality,
174+
'minEdge': minEdge,
175+
'maxEdge': maxEdge,
176+
'minDihedral': minDihedral,
177+
'maxDihedral': maxDihedral,
178+
'dihedralRange': dihedralRange
179+
}
180+
181+
182+
# Combined quality score
183+
def computeQualityScore( aspectRatio: npt.NDArray[ np.float64 ], shapeQuality: npt.NDArray[ np.float64 ],
184+
edgeRatio: npt.NDArray[ np.float64 ],
185+
minDihedralAngle: npt.NDArray[ np.float64 ] ) -> npt.NDArray[ np.float64 ]:
186+
"""Compute combined quality score (0-100) from aspect ratio, shape quality, minimal dihedral angle and edge ratio.
187+
188+
The quality score can be interpreted with the following scale:
189+
Excellent (>80)
190+
Good (60-80)
191+
Fair (30-60)
192+
Poor (≤30)
193+
194+
Args:
195+
aspectRatio(npt.NDArray[np.float64]): Aspect ratio
196+
shapeQuality(npt.NDArray[np.float64]): Shape quality
197+
edgeRatio(npt.NDArray[np.float64]): Edge ratio
198+
minDihedralAngle(npt.NDArray[np.float64]): Minimal edge ratio
199+
200+
Returns:
201+
np.float64: Quality score
202+
"""
203+
aspectRatioNorm = np.clip( 1.0 / ( aspectRatio / 1.73 ), 0, 1 )
204+
shapeQualityNorm = shapeQuality
205+
edgeRatioNorm = np.clip( 1.0 / edgeRatio, 0, 1 )
206+
dihedralMinNorm = np.clip( minDihedralAngle / 60.0, 0, 1 )
207+
score = ( 0.3 * aspectRatioNorm + 0.4 * shapeQualityNorm + 0.2 * edgeRatioNorm + 0.1 * dihedralMinNorm ) * 100
208+
209+
return score

0 commit comments

Comments
 (0)