Skip to content

Commit 0cde8ff

Browse files
refactor: Move and refactor Surface Geomechanics vtk filter and plugin (#133)
* Move SurfaceGeomechanics * Refactor the basis change computation * Refactor of normal and tangential vectors computation * Add tests and better error handling
1 parent 6fe3c7b commit 0cde8ff

File tree

21 files changed

+1441
-855
lines changed

21 files changed

+1441
-855
lines changed

docs/geos_posp_docs/PVplugins.rst

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,3 @@ PVMohrCirclePlot plugin
6262
---------------------------------
6363

6464
.. automodule:: PVplugins.PVMohrCirclePlot
65-
66-
PVplugins.PVSurfaceGeomechanics module
67-
--------------------------------------
68-
69-
.. automodule:: PVplugins.PVSurfaceGeomechanics
70-

docs/geos_posp_docs/filters.rst

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,3 @@ geos_posp.filters.GeosBlockMerge module
1010
:members:
1111
:undoc-members:
1212
:show-inheritance:
13-
14-
geos_posp.filters.SurfaceGeomechanics module
15-
------------------------------------------------
16-
17-
.. automodule:: geos_posp.filters.SurfaceGeomechanics
18-
:members:
19-
:undoc-members:
20-
:show-inheritance:

docs/geos_processing_docs/post_processing.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,28 @@ This module contains GEOS post-processing tools.
66
Geomechanics post-processing tools
77
=====================================
88

9+
GEOS computes many outputs including flow and geomechanic properties if coupled flow geomechanics simulations were run. Users however need additional metrics to asses geomechanical stability as part of operational studies. Two filters have been developped to compute these additional metrics: `Geos Geomechanics Calculator`` and `Geos Surfacic Geomechanics`. In addition, a third filter allows to plot Mohr's circles on selected cells and time steps.
10+
11+
.. Note::
12+
13+
Several processing filters require the definition of physical parameters. The following list is non-exhaustive.
14+
15+
Default values:
16+
- grainBulkModulus = 38e9 Pa ( quartz value )
17+
- specificDensity = 1000.0 kg/m³ ( water value )
18+
- rock cohesion: 0.0 Pa $( fractured case )
19+
- friction angle: 10°
20+
21+
22+
geos.processing.post_processing.SurfaceGeomechanics
23+
-------------------------------------------------------
24+
25+
.. automodule:: geos.processing.post_processing.SurfaceGeomechanics
26+
:members:
27+
:undoc-members:
28+
:show-inheritance:
29+
30+
931
geos.processing.post_processing.GeomechanicsCalculator module
1032
--------------------------------------------------------------
1133

@@ -14,6 +36,7 @@ geos.processing.post_processing.GeomechanicsCalculator module
1436
:undoc-members:
1537
:show-inheritance:
1638

39+
1740
geos.processing.post_processing.GeosBlockExtractor module
1841
----------------------------------------------------------
1942

geos-mesh/src/geos/mesh/utils/genericHelpers.py

Lines changed: 284 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,34 @@
22
# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
33
# SPDX-FileContributor: Martin Lemay, Paloma Martinez
44
import numpy as np
5+
import logging
56
import numpy.typing as npt
6-
from typing import Iterator, List, Sequence, Any, Union
7-
from vtkmodules.util.numpy_support import numpy_to_vtk
8-
from vtkmodules.vtkCommonCore import vtkIdList, vtkPoints, reference
9-
from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid, vtkMultiBlockDataSet, vtkPolyData, vtkDataSet, vtkDataObject, vtkPlane, vtkCellTypes, vtkIncrementalOctreePointLocator
10-
from vtkmodules.vtkFiltersCore import vtk3DLinearGridPlaneCutter
7+
from typing import Iterator, List, Sequence, Any, Union, Tuple
8+
from vtkmodules.util.numpy_support import ( numpy_to_vtk, vtk_to_numpy )
9+
from vtkmodules.vtkCommonCore import vtkIdList, vtkPoints, reference, vtkDataArray, vtkLogger, vtkFloatArray
10+
from vtkmodules.vtkCommonDataModel import (
11+
vtkUnstructuredGrid,
12+
vtkMultiBlockDataSet,
13+
vtkPolyData,
14+
vtkDataSet,
15+
vtkDataObject,
16+
vtkPlane,
17+
vtkCellTypes,
18+
vtkIncrementalOctreePointLocator,
19+
)
20+
from vtkmodules.vtkFiltersCore import ( vtk3DLinearGridPlaneCutter, vtkPolyDataNormals, vtkPolyDataTangents )
21+
from vtkmodules.vtkFiltersTexture import vtkTextureMapToPlane
22+
1123
from geos.mesh.utils.multiblockHelpers import ( getBlockElementIndexesFlatten, getBlockFromFlatIndex )
1224

25+
from geos.utils.algebraFunctions import (
26+
getAttributeMatrixFromVector,
27+
getAttributeVectorFromMatrix,
28+
)
29+
from geos.utils.geometryFunctions import ( getChangeOfBasisMatrix, CANONICAL_BASIS_3D )
30+
from geos.utils.Logger import ( getLogger, Logger, VTKCaptureLog, RegexExceptionFilter )
31+
from geos.utils.Errors import VTKError
32+
1333
__doc__ = """
1434
Generic VTK utilities.
1535
@@ -267,3 +287,262 @@ def createVertices( cellPtsCoord: list[ npt.NDArray[ np.float64 ] ],
267287
cellVertexMap += [ ptId.get() ] # type: ignore
268288
cellVertexMapAll += [ tuple( cellVertexMap ) ]
269289
return points, cellVertexMapAll
290+
291+
292+
def convertAttributeFromLocalToXYZForOneCell(
293+
vector: npt.NDArray[ np.float64 ], localBasisVectors: tuple[ npt.NDArray[ np.float64 ], npt.NDArray[ np.float64 ],
294+
npt.NDArray[ np.float64 ] ]
295+
) -> npt.NDArray[ np.float64 ]:
296+
"""Convert one cell attribute from local to XYZ basis.
297+
298+
.. Warning::
299+
Vectors components are considered to be in GEOS output order such that \
300+
v = ( M00, M11, M22, M12, M02, M01, M21, M20, M10 )
301+
302+
Args:
303+
vector (npt.NDArray[np.float64]): The attribute expressed in local basis. The size of the vector must be 3, 9 or 6 (symmetrical case)
304+
localBasisVectors (tuple[ npt.NDArray[np.float64], npt.NDArray[np.float64], npt.NDArray[np.float64]]): The local basis vectors expressed in the XYZ basis.
305+
306+
Returns:
307+
vectorXYZ (npt.NDArray[np.float64]): The attribute expressed in XYZ basis.
308+
"""
309+
# Get components matrix from GEOS attribute vector
310+
matrix3x3: npt.NDArray[ np.float64 ] = getAttributeMatrixFromVector( vector )
311+
312+
# Get transform matrix
313+
transformMatrix: npt.NDArray[ np.float64 ] = getChangeOfBasisMatrix( localBasisVectors, CANONICAL_BASIS_3D )
314+
315+
# Apply transformation
316+
arrayXYZ: npt.NDArray[ np.float64 ] = transformMatrix @ matrix3x3 @ transformMatrix.T
317+
318+
# Convert back to GEOS type attribute and return
319+
return getAttributeVectorFromMatrix( arrayXYZ, vector.size )
320+
321+
322+
def getNormalVectors( surface: vtkPolyData ) -> npt.NDArray[ np.float64 ]:
323+
"""Return the normal vectors of a surface mesh.
324+
325+
Args:
326+
surface (vtkPolyData): The input surface.
327+
328+
Raises:
329+
ValueError: No normal attribute found in the mesh. Use the computeNormals function beforehand.
330+
331+
Returns:
332+
npt.NDArray[ np.float64 ]: The normal vectors of the input surface.
333+
"""
334+
normals: Union[ npt.NDArray[ np.float64 ],
335+
None ] = surface.GetCellData().GetNormals() # type: ignore[no-untyped-call]
336+
if normals is None:
337+
raise ValueError( "No normal attribute found in the mesh. Use the computeNormals function beforehand." )
338+
339+
return vtk_to_numpy( normals )
340+
341+
342+
def getTangentsVectors( surface: vtkPolyData ) -> Tuple[ npt.NDArray[ np.float64 ], npt.NDArray[ np.float64 ] ]:
343+
"""Return the tangential vectors of a surface.
344+
345+
Args:
346+
surface (vtkPolyData): The input surface.
347+
348+
Raises:
349+
ValueError: Tangent attribute not found in the mesh. Use computeTangents beforehand.
350+
ValueError: Problem during the calculation of the second tangential vectors.
351+
352+
Returns:
353+
Tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: The tangents vectors of the input surface.
354+
"""
355+
# Get first tangential component
356+
vtkTangents: Union[ vtkFloatArray, None ] = surface.GetCellData().GetTangents() # type: ignore[no-untyped-call]
357+
tangents1: npt.NDArray[ np.float64 ]
358+
359+
try:
360+
tangents1 = vtk_to_numpy( vtkTangents )
361+
except AttributeError as err:
362+
print( "No tangential attribute found in the mesh. Use the computeTangents function beforehand." )
363+
raise VTKError( err ) from err
364+
else:
365+
# Compute second tangential component
366+
normals: npt.NDArray[ np.float64 ] = getNormalVectors( surface )
367+
368+
tangents2: npt.NDArray[ np.float64 ] = np.cross( normals, tangents1, axis=1 ).astype( np.float64 )
369+
370+
return ( tangents1, tangents2 )
371+
372+
373+
def getLocalBasisVectors( surface: vtkPolyData ) -> npt.NDArray[ np.float64 ]:
374+
"""Return the local basis vectors for all cells of the input surface.
375+
376+
Args:
377+
surface(vtkPolydata): The input surface.
378+
379+
Returns:
380+
npt.NDArray[np.float64]: Array with normal, tangential 1 and tangential 2 vectors.
381+
"""
382+
try:
383+
normals: npt.NDArray[ np.float64 ] = getNormalVectors( surface )
384+
surfaceWithNormals: vtkPolyData = surface
385+
# ValueError raised if no normals found in the mesh
386+
except ValueError:
387+
# In that case, the normals are computed.
388+
surfaceWithNormals = computeNormals( surface )
389+
normals = getNormalVectors( surfaceWithNormals )
390+
391+
# Tangents require normals to be present in the mesh
392+
try:
393+
tangents: Tuple[ npt.NDArray[ np.float64 ],
394+
npt.NDArray[ np.float64 ] ] = getTangentsVectors( surfaceWithNormals )
395+
# If no tangents is present in the mesh, they are computed on that surface
396+
except VTKError:
397+
surfaceWithTangents: vtkPolyData = computeTangents( surfaceWithNormals )
398+
tangents = getTangentsVectors( surfaceWithTangents )
399+
400+
return np.array( ( normals, *tangents ) )
401+
402+
403+
def computeNormals(
404+
surface: vtkPolyData,
405+
logger: Union[ Logger, None ] = None,
406+
) -> vtkPolyData:
407+
"""Compute and set the normals of a given surface.
408+
409+
Args:
410+
surface (vtkPolyData): The input surface.
411+
logger (Union[Logger, None], optional): A logger to manage the output messages.
412+
Defaults to None, an internal logger is used.
413+
414+
Returns:
415+
vtkPolyData: The surface with normal attribute.
416+
"""
417+
if logger is None:
418+
logger = getLogger( "computeSurfaceNormals" )
419+
# Creation of a child logger to deal with VTKErrors without polluting parent logger
420+
vtkErrorLogger: Logger = getLogger( f"{logger.name}.vtkErrorLogger" )
421+
vtkErrorLogger.propagate = False
422+
423+
vtkLogger.SetStderrVerbosity( vtkLogger.VERBOSITY_ERROR )
424+
425+
vtkErrorLogger.addFilter( RegexExceptionFilter() ) # will raise VTKError if captured VTK Error
426+
vtkErrorLogger.setLevel( logging.DEBUG )
427+
428+
with VTKCaptureLog() as capturedLog:
429+
normalFilter: vtkPolyDataNormals = vtkPolyDataNormals()
430+
normalFilter.SetInputData( surface )
431+
normalFilter.ComputeCellNormalsOn()
432+
normalFilter.ComputePointNormalsOff()
433+
normalFilter.Update()
434+
435+
capturedLog.seek( 0 )
436+
captured = capturedLog.read().decode()
437+
438+
vtkErrorLogger.debug( captured.strip() )
439+
440+
outputSurface = normalFilter.GetOutput()
441+
442+
if outputSurface.GetCellData().GetNormals() is None:
443+
raise VTKError( "Something went wrong with VTK calculation of the normals." )
444+
445+
return outputSurface
446+
447+
448+
def computeTangents(
449+
triangulatedSurface: vtkPolyData,
450+
logger: Union[ Logger, None ] = None,
451+
) -> vtkPolyData:
452+
"""Compute and set the tangents of a given surface.
453+
454+
.. Warning:: The computation of tangents requires a triangulated surface.
455+
456+
Args:
457+
triangulatedSurface (vtkPolyData): The input surface. It should be triangulated beforehand.
458+
logger (Union[Logger, None], optional): A logger to manage the output messages.
459+
Defaults to None, an internal logger is used.
460+
461+
Returns:
462+
vtkPolyData: The surface with tangent attribute
463+
"""
464+
# need to compute texture coordinates required for tangent calculation
465+
surface1: vtkPolyData = computeSurfaceTextureCoordinates( triangulatedSurface )
466+
467+
# TODO: triangulate the surface before computation of the tangents if needed.
468+
469+
# compute tangents
470+
if logger is None:
471+
logger = getLogger( "computeSurfaceTangents" )
472+
# Creation of a child logger to deal with VTKErrors without polluting parent logger
473+
vtkErrorLogger: Logger = getLogger( f"{logger.name}.vtkErrorLogger" )
474+
vtkErrorLogger.propagate = False
475+
476+
vtkLogger.SetStderrVerbosity( vtkLogger.VERBOSITY_ERROR )
477+
478+
vtkErrorLogger.addFilter( RegexExceptionFilter() ) # will raise VTKError if captured VTK Error
479+
vtkErrorLogger.setLevel( logging.DEBUG )
480+
481+
with VTKCaptureLog() as capturedLog:
482+
483+
tangentFilter: vtkPolyDataTangents = vtkPolyDataTangents()
484+
tangentFilter.SetInputData( surface1 )
485+
tangentFilter.ComputeCellTangentsOn()
486+
tangentFilter.ComputePointTangentsOff()
487+
tangentFilter.Update()
488+
surfaceOut: vtkPolyData = tangentFilter.GetOutput()
489+
490+
capturedLog.seek( 0 )
491+
captured = capturedLog.read().decode()
492+
493+
vtkErrorLogger.debug( captured.strip() )
494+
495+
if surfaceOut is None:
496+
raise VTKError( "Something went wrong in VTK calculation." )
497+
498+
# copy tangents attributes into filter input surface because surface attributes
499+
# are not transferred to the output (bug that should be corrected by Kitware)
500+
array: vtkDataArray = surfaceOut.GetCellData().GetTangents()
501+
if array is None:
502+
raise VTKError( "Attribute Tangents is not in the mesh." )
503+
504+
surface1.GetCellData().SetTangents( array )
505+
surface1.GetCellData().Modified()
506+
surface1.Modified()
507+
return surface1
508+
509+
510+
def computeSurfaceTextureCoordinates(
511+
surface: vtkPolyData,
512+
logger: Union[ Logger, None ] = None,
513+
) -> vtkPolyData:
514+
"""Generate the 2D texture coordinates required for tangent computation. The dataset points are mapped onto a plane generated automatically.
515+
516+
Args:
517+
surface (vtkPolyData): The input surface.
518+
logger (Union[Logger, None], optional): A logger to manage the output messages.
519+
Defaults to None, an internal logger is used.
520+
521+
Returns:
522+
vtkPolyData: The input surface with generated texture map.
523+
"""
524+
# Need to compute texture coordinates required for tangent calculation
525+
if logger is None:
526+
logger = getLogger( "computeSurfaceTextureCoordinates" )
527+
# Creation of a child logger to deal with VTKErrors without polluting parent logger
528+
vtkErrorLogger: Logger = getLogger( f"{logger.name}.vtkErrorLogger" )
529+
vtkErrorLogger.propagate = False
530+
531+
vtkLogger.SetStderrVerbosity( vtkLogger.VERBOSITY_ERROR )
532+
533+
vtkErrorLogger.addFilter( RegexExceptionFilter() ) # will raise VTKError if captured VTK Error
534+
vtkErrorLogger.setLevel( logging.DEBUG )
535+
536+
with VTKCaptureLog() as capturedLog:
537+
538+
textureFilter: vtkTextureMapToPlane = vtkTextureMapToPlane()
539+
textureFilter.SetInputData( surface )
540+
textureFilter.AutomaticPlaneGenerationOn()
541+
textureFilter.Update()
542+
543+
capturedLog.seek( 0 )
544+
captured = capturedLog.read().decode()
545+
546+
vtkErrorLogger.debug( captured.strip() )
547+
548+
return textureFilter.GetOutput()

geos-mesh/tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# SPDX-License-Identifier: Apache-2.0
22
# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
3-
# SPDX-FileContributor: Paloma Martinez
3+
# SPDX-FileContributor: Paloma Martinez, Romain Baville
44
# SPDX-License-Identifier: Apache 2.0
55
# ruff: noqa: E402 # disable Module level import not at top of file
66
import os

0 commit comments

Comments
 (0)