diff --git a/geos-processing/src/geos/processing/generic_processing_tools/SplitMesh.py b/geos-processing/src/geos/processing/generic_processing_tools/SplitMesh.py index 2695c8f8..2d917027 100644 --- a/geos-processing/src/geos/processing/generic_processing_tools/SplitMesh.py +++ b/geos-processing/src/geos/processing/generic_processing_tools/SplitMesh.py @@ -83,6 +83,13 @@ def __init__( self, inputMesh: vtkUnstructuredGrid, speHandler: bool = False ) - self.logger = logging.getLogger( loggerTitle ) self.logger.setLevel( logging.INFO ) self.logger.propagate = False + handlers: list[ logging.Handler ] = self.logger.handlers + # Get the handler to specify if the logger already exist and has it + for handler in handlers: + # The CountWarningHandler can't be the handler to specify + if type( handler ) is not type( CountWarningHandler() ): + self.handler = handler + break counter: CountWarningHandler = CountWarningHandler() self.counter: CountWarningHandler diff --git a/geos-processing/src/geos/processing/pre_processing/MeshQualityEnhanced.py b/geos-processing/src/geos/processing/pre_processing/MeshQualityEnhanced.py index 0e75daff..3f75cfae 100644 --- a/geos-processing/src/geos/processing/pre_processing/MeshQualityEnhanced.py +++ b/geos-processing/src/geos/processing/pre_processing/MeshQualityEnhanced.py @@ -132,6 +132,7 @@ def __init__( self._allCellTypesExtended: tuple[ int, ...] = getAllCellTypesExtended() self._allCellTypes: tuple[ int, ...] = getAllCellTypes() self.speHandler: bool = speHandler + self.handler: logging.Handler # Logger self.logger: Logger @@ -141,6 +142,13 @@ def __init__( self.logger = logging.getLogger( loggerTitle ) self.logger.setLevel( logging.INFO ) self.logger.propagate = False + handlers: list[ logging.Handler ] = self.logger.handlers + # Get the handler to specify if the logger already exist and has it + for handler in handlers: + # The CountWarningHandler can't be the handler to specify + if type( handler ) is not type( CountWarningHandler() ): + self.handler = handler + break counter: CountWarningHandler = CountWarningHandler() self.counter: CountWarningHandler diff --git a/geos-pv/src/geos/pv/plugins/generic_processing/PVAttributeMapping.py b/geos-pv/src/geos/pv/plugins/generic_processing/PVAttributeMapping.py index d6b07cab..20c29a0b 100644 --- a/geos-pv/src/geos/pv/plugins/generic_processing/PVAttributeMapping.py +++ b/geos-pv/src/geos/pv/plugins/generic_processing/PVAttributeMapping.py @@ -54,6 +54,8 @@ """ +HANDLER: logging.Handler = VTKHandler() + @smproxy.filter( name="PVAttributeMapping", label="Attribute Mapping" ) @smhint.xml( f'' ) @@ -76,7 +78,6 @@ def __init__( self: Self ) -> None: self.piece: Piece = Piece.CELLS self.clearAttributeNames = True self.attributeNames: list[ str ] = [] - self.handler: logging.Handler = VTKHandler() @smproperty.intvector( name="AttributePiece", @@ -190,8 +191,8 @@ def RequestData( attributeMappingFilter: AttributeMapping = AttributeMapping( meshFrom, outData, set( self.attributeNames ), self.piece, True ) - if not isHandlerInLogger( self.handler, attributeMappingFilter.logger ): - attributeMappingFilter.setLoggerHandler( self.handler ) + if not isHandlerInLogger( HANDLER, attributeMappingFilter.logger ): + attributeMappingFilter.setLoggerHandler( HANDLER ) try: attributeMappingFilter.applyFilter() diff --git a/geos-pv/src/geos/pv/plugins/generic_processing/PVClipToMainFrame.py b/geos-pv/src/geos/pv/plugins/generic_processing/PVClipToMainFrame.py index 4c04589a..3461b134 100644 --- a/geos-pv/src/geos/pv/plugins/generic_processing/PVClipToMainFrame.py +++ b/geos-pv/src/geos/pv/plugins/generic_processing/PVClipToMainFrame.py @@ -36,6 +36,8 @@ """ +HANDLER: logging.Handler = VTKHandler() + @SISOFilter( category=FilterCategory.GENERIC_PROCESSING, decoratedLabel="Clip to the main frame", @@ -45,10 +47,9 @@ class PVClipToMainFrame( VTKPythonAlgorithmBase ): def __init__( self ) -> None: """Init motherclass, filter and logger.""" self._realFilter = ClipToMainFrame( speHandler=True ) - self.handler: logging.Handler = VTKHandler() - if not isHandlerInLogger( self.handler, self._realFilter.logger ): - self._realFilter.SetLoggerHandler( self.handler ) + if not isHandlerInLogger( HANDLER, self._realFilter.logger ): + self._realFilter.SetLoggerHandler( HANDLER ) def ApplyFilter( self, inputMesh: vtkMultiBlockDataSet, outputMesh: vtkMultiBlockDataSet ) -> None: """Is applying clipToMainFrame filter. diff --git a/geos-pv/src/geos/pv/plugins/generic_processing/PVCreateConstantAttributePerRegion.py b/geos-pv/src/geos/pv/plugins/generic_processing/PVCreateConstantAttributePerRegion.py index 372ea2c3..bb8973aa 100644 --- a/geos-pv/src/geos/pv/plugins/generic_processing/PVCreateConstantAttributePerRegion.py +++ b/geos-pv/src/geos/pv/plugins/generic_processing/PVCreateConstantAttributePerRegion.py @@ -50,6 +50,8 @@ """ +HANDLER: logging.Handler = VTKHandler() + @SISOFilter( category=FilterCategory.GENERIC_PROCESSING, decoratedLabel="Create Constant Attribute Per Region", @@ -72,7 +74,6 @@ def __init__( self: Self ) -> None: # Use the handler of paraview for the log. self.speHandler: bool = True - self.handler: logging.Handler = VTKHandler() # Settings of the attribute with the region indexes: @smproperty.stringvector( @@ -292,8 +293,8 @@ def ApplyFilter( self, inputMesh: vtkDataSet, outputMesh: vtkDataSet ) -> None: self.speHandler, ) - if not isHandlerInLogger( self.handler, createConstantAttributePerRegionFilter.logger ): - createConstantAttributePerRegionFilter.setLoggerHandler( self.handler ) + if not isHandlerInLogger( HANDLER, createConstantAttributePerRegionFilter.logger ): + createConstantAttributePerRegionFilter.setLoggerHandler( HANDLER ) try: createConstantAttributePerRegionFilter.applyFilter() diff --git a/geos-pv/src/geos/pv/plugins/generic_processing/PVFillPartialArrays.py b/geos-pv/src/geos/pv/plugins/generic_processing/PVFillPartialArrays.py index 547945aa..4a6e27d2 100644 --- a/geos-pv/src/geos/pv/plugins/generic_processing/PVFillPartialArrays.py +++ b/geos-pv/src/geos/pv/plugins/generic_processing/PVFillPartialArrays.py @@ -41,6 +41,8 @@ """ +HANDLER: logging.Handler = VTKHandler() + @SISOFilter( category=FilterCategory.GENERIC_PROCESSING, decoratedLabel="Fill Partial Arrays", @@ -51,7 +53,6 @@ def __init__( self: Self, ) -> None: """Fill a partial attribute with constant value per component.""" self.clearDictAttributesValues: bool = True self.dictAttributesValues: dict[ str, Union[ list[ Any ], None ] ] = {} - self.handler: logging.Handler = VTKHandler() @smproperty.xml( """ ' ) @@ -68,7 +70,6 @@ def __init__( self: Self ) -> None: inputType="vtkMultiBlockDataSet", outputType="vtkUnstructuredGrid", ) - self.handler: logging.Handler = VTKHandler() def RequestDataObject( self: Self, @@ -118,8 +119,8 @@ def RequestData( mergeBlockEnhancedFilter: MergeBlockEnhanced = MergeBlockEnhanced( inputMesh, True ) - if not isHandlerInLogger( self.handler, mergeBlockEnhancedFilter.logger ): - mergeBlockEnhancedFilter.setLoggerHandler( self.handler ) + if not isHandlerInLogger( HANDLER, mergeBlockEnhancedFilter.logger ): + mergeBlockEnhancedFilter.setLoggerHandler( HANDLER ) try: mergeBlockEnhancedFilter.applyFilter() diff --git a/geos-pv/src/geos/pv/plugins/generic_processing/PVSplitMesh.py b/geos-pv/src/geos/pv/plugins/generic_processing/PVSplitMesh.py index 81da5b81..7a3b15d5 100644 --- a/geos-pv/src/geos/pv/plugins/generic_processing/PVSplitMesh.py +++ b/geos-pv/src/geos/pv/plugins/generic_processing/PVSplitMesh.py @@ -39,13 +39,14 @@ """ +HANDLER: logging.Handler = VTKHandler() + @SISOFilter( category=FilterCategory.GENERIC_PROCESSING, decoratedLabel="Split Mesh", decoratedType="vtkPointSet" ) class PVSplitMesh( VTKPythonAlgorithmBase ): def __init__( self: Self ) -> None: """Split mesh cells.""" - self.handler: logging.Handler = VTKHandler() def ApplyFilter( self: Self, inputMesh: vtkPointSet, outputMesh: vtkPointSet ) -> None: """Apply vtk filter. @@ -55,8 +56,8 @@ def ApplyFilter( self: Self, inputMesh: vtkPointSet, outputMesh: vtkPointSet ) - outputMesh: Output mesh. """ splitMeshFilter: SplitMesh = SplitMesh( inputMesh, True ) - if not isHandlerInLogger( self.handler, splitMeshFilter.logger ): - splitMeshFilter.setLoggerHandler( self.handler ) + if not isHandlerInLogger( HANDLER, splitMeshFilter.logger ): + splitMeshFilter.setLoggerHandler( HANDLER ) try: splitMeshFilter.applyFilter() diff --git a/geos-pv/src/geos/pv/plugins/post_processing/PVGeomechanicsCalculator.py b/geos-pv/src/geos/pv/plugins/post_processing/PVGeomechanicsCalculator.py index 693a5f5f..0f707256 100644 --- a/geos-pv/src/geos/pv/plugins/post_processing/PVGeomechanicsCalculator.py +++ b/geos-pv/src/geos/pv/plugins/post_processing/PVGeomechanicsCalculator.py @@ -73,6 +73,8 @@ """ +HANDLER: logging.Handler = VTKHandler() + loggerTitle: str = "Geomechanics Calculator" @@ -93,10 +95,9 @@ def __init__( self: Self ) -> None: self.rockCohesion: float = DEFAULT_ROCK_COHESION self.frictionAngle: float = DEFAULT_FRICTION_ANGLE_DEG - self.handler: logging.Handler = VTKHandler() self.logger = logging.getLogger( loggerTitle ) self.logger.setLevel( logging.INFO ) - self.logger.addHandler( self.handler ) + self.logger.addHandler( HANDLER ) self.logger.propagate = False counter: CountWarningHandler = CountWarningHandler() @@ -265,8 +266,8 @@ def ApplyFilter( speHandler=True, ) - if not isHandlerInLogger( self.handler, geomechanicsCalculatorFilter.logger ): - geomechanicsCalculatorFilter.setLoggerHandler( self.handler ) + if not isHandlerInLogger( HANDLER, geomechanicsCalculatorFilter.logger ): + geomechanicsCalculatorFilter.setLoggerHandler( HANDLER ) geomechanicsCalculatorFilter.physicalConstants.grainBulkModulus = self.grainBulkModulus geomechanicsCalculatorFilter.physicalConstants.specificDensity = self.specificDensity @@ -303,8 +304,8 @@ def ApplyFilter( True, ) - if not isHandlerInLogger( self.handler, geomechanicsCalculatorFilter.logger ): - geomechanicsCalculatorFilter.setLoggerHandler( self.handler ) + if not isHandlerInLogger( HANDLER, geomechanicsCalculatorFilter.logger ): + geomechanicsCalculatorFilter.setLoggerHandler( HANDLER ) geomechanicsCalculatorFilter.physicalConstants.grainBulkModulus = self.grainBulkModulus geomechanicsCalculatorFilter.physicalConstants.specificDensity = self.specificDensity diff --git a/geos-pv/src/geos/pv/plugins/post_processing/PVGeomechanicsWorkflow.py b/geos-pv/src/geos/pv/plugins/post_processing/PVGeomechanicsWorkflow.py index 0b446062..efb1c089 100644 --- a/geos-pv/src/geos/pv/plugins/post_processing/PVGeomechanicsWorkflow.py +++ b/geos-pv/src/geos/pv/plugins/post_processing/PVGeomechanicsWorkflow.py @@ -85,6 +85,7 @@ """ +HANDLER: logging.Handler = VTKHandler() loggerTitle: str = "GEOS Geomechanics Workflow" @@ -139,10 +140,9 @@ def __init__( self: Self ) -> None: self.rockCohesion: float = DEFAULT_ROCK_COHESION self.frictionAngle: float = DEFAULT_FRICTION_ANGLE_DEG - self.handler: logging.Handler = VTKHandler() self.logger = logging.getLogger( loggerTitle ) self.logger.setLevel( logging.INFO ) - self.logger.addHandler( self.handler ) + self.logger.addHandler( HANDLER ) self.logger.propagate = False counter: CountWarningHandler = CountWarningHandler() diff --git a/geos-pv/src/geos/pv/plugins/post_processing/PVGeosBlockExtractAndMerge.py b/geos-pv/src/geos/pv/plugins/post_processing/PVGeosBlockExtractAndMerge.py index dff27de7..62e415a8 100644 --- a/geos-pv/src/geos/pv/plugins/post_processing/PVGeosBlockExtractAndMerge.py +++ b/geos-pv/src/geos/pv/plugins/post_processing/PVGeosBlockExtractAndMerge.py @@ -82,6 +82,7 @@ """ +HANDLER: logging.Handler = VTKHandler() loggerTitle: str = "Extract & Merge GEOS Block" @@ -129,10 +130,9 @@ def __init__( self: Self ) -> None: self.outputCellsT0: vtkMultiBlockDataSet = vtkMultiBlockDataSet() - self.handler: logging.Handler = VTKHandler() self.logger = logging.getLogger( loggerTitle ) self.logger.setLevel( logging.INFO ) - self.logger.addHandler( self.handler ) + self.logger.addHandler( HANDLER ) self.logger.propagate = False counter: CountWarningHandler = CountWarningHandler() diff --git a/geos-pv/src/geos/pv/plugins/post_processing/PVGeosLogReader.py b/geos-pv/src/geos/pv/plugins/post_processing/PVGeosLogReader.py index 4b8aea9f..b46fd463 100644 --- a/geos-pv/src/geos/pv/plugins/post_processing/PVGeosLogReader.py +++ b/geos-pv/src/geos/pv/plugins/post_processing/PVGeosLogReader.py @@ -60,6 +60,7 @@ """ +HANDLER: logging.Handler = VTKHandler() loggerTitle: str = "Geos Log Reader" @@ -150,10 +151,9 @@ def __init__( self: Self ) -> None: for prop in propsSolvers: self.m_convergence.AddArray( prop ) - self.handler: logging.Handler = VTKHandler() self.logger = logging.getLogger( loggerTitle ) self.logger.setLevel( logging.INFO ) - self.logger.addHandler( self.handler ) + self.logger.addHandler( HANDLER ) self.logger.propagate = False counter: CountWarningHandler = CountWarningHandler() diff --git a/geos-pv/src/geos/pv/plugins/post_processing/PVMohrCirclePlot.py b/geos-pv/src/geos/pv/plugins/post_processing/PVMohrCirclePlot.py index e3ff2585..625ac5c3 100644 --- a/geos-pv/src/geos/pv/plugins/post_processing/PVMohrCirclePlot.py +++ b/geos-pv/src/geos/pv/plugins/post_processing/PVMohrCirclePlot.py @@ -91,6 +91,7 @@ * The attribute 'CellId' has to be used for the 'Selection Labels'. """ +HANDLER: logging.Handler = VTKHandler() loggerTitle: str = "Mohr Circle" @@ -181,10 +182,9 @@ def __init__( self: Self ) -> None: # Request data processing step - incremented each time RequestUpdateExtent is called self.requestDataStep: int = -1 - self.handler: logging.Handler = VTKHandler() self.logger = logging.getLogger( loggerTitle ) self.logger.setLevel( logging.INFO ) - self.logger.addHandler( self.handler ) + self.logger.addHandler( HANDLER ) self.logger.propagate = False counter: CountWarningHandler = CountWarningHandler() diff --git a/geos-pv/src/geos/pv/plugins/post_processing/PVSurfaceGeomechanics.py b/geos-pv/src/geos/pv/plugins/post_processing/PVSurfaceGeomechanics.py index 2cbc5ba2..f63c2b82 100644 --- a/geos-pv/src/geos/pv/plugins/post_processing/PVSurfaceGeomechanics.py +++ b/geos-pv/src/geos/pv/plugins/post_processing/PVSurfaceGeomechanics.py @@ -52,6 +52,7 @@ """ +HANDLER: logging.Handler = VTKHandler() loggerTitle: str = "Surface Geomechanics" @@ -70,10 +71,9 @@ def __init__( self: Self ) -> None: # friction angle (°) self.frictionAngle: float = DEFAULT_FRICTION_ANGLE_DEG - self.handler: logging.Handler = VTKHandler() self.logger = logging.getLogger( loggerTitle ) self.logger.setLevel( logging.INFO ) - self.logger.addHandler( self.handler ) + self.logger.addHandler( HANDLER ) self.logger.propagate = False counter: CountWarningHandler = CountWarningHandler() @@ -148,8 +148,8 @@ def ApplyFilter( self: Self, inputMesh: vtkMultiBlockDataSet, outputMesh: vtkMul loggerName: str = f"Surface geomechanics for the blockIndex { blockIndex }" sgFilter: SurfaceGeomechanics = SurfaceGeomechanics( surfaceBlock, loggerName, True ) - if not isHandlerInLogger( self.handler, sgFilter.logger ): - sgFilter.SetLoggerHandler( self.handler ) + if not isHandlerInLogger( HANDLER, sgFilter.logger ): + sgFilter.SetLoggerHandler( HANDLER ) sgFilter.SetRockCohesion( self._getRockCohesion() ) sgFilter.SetFrictionAngle( self._getFrictionAngle() ) diff --git a/geos-pv/src/geos/pv/plugins/qc/PVCellTypeCounterEnhanced.py b/geos-pv/src/geos/pv/plugins/qc/PVCellTypeCounterEnhanced.py index 44e99ced..c46c429a 100644 --- a/geos-pv/src/geos/pv/plugins/qc/PVCellTypeCounterEnhanced.py +++ b/geos-pv/src/geos/pv/plugins/qc/PVCellTypeCounterEnhanced.py @@ -41,6 +41,8 @@ """ +HANDLER: logging.Handler = VTKHandler() + @smproxy.filter( name="PVCellTypeCounterEnhanced", label="Cell Type Counter Enhanced" ) @smhint.xml( f'' ) @@ -60,8 +62,6 @@ def __init__( self: Self ) -> None: # used to concatenate results if vtkMultiBlockDataSet self._countsAll: CellTypeCounts = CellTypeCounts() - self.handler: logging.Handler = VTKHandler() - @smproperty.intvector( name="SetSaveToFile", label="Save to file", @@ -141,8 +141,8 @@ def RequestData( assert outputTable is not None, "Output pipeline is null." cellTypeCounterEnhancedFilter: CellTypeCounterEnhanced = CellTypeCounterEnhanced( inputMesh, True ) - if not isHandlerInLogger( self.handler, cellTypeCounterEnhancedFilter.logger ): - cellTypeCounterEnhancedFilter.setLoggerHandler( self.handler ) + if not isHandlerInLogger( HANDLER, cellTypeCounterEnhancedFilter.logger ): + cellTypeCounterEnhancedFilter.setLoggerHandler( HANDLER ) try: cellTypeCounterEnhancedFilter.applyFilter() diff --git a/geos-pv/src/geos/pv/plugins/qc/PVMeshQualityEnhanced.py b/geos-pv/src/geos/pv/plugins/qc/PVMeshQualityEnhanced.py index d959b1e8..2c00aadc 100644 --- a/geos-pv/src/geos/pv/plugins/qc/PVMeshQualityEnhanced.py +++ b/geos-pv/src/geos/pv/plugins/qc/PVMeshQualityEnhanced.py @@ -58,6 +58,8 @@ Please refer to the `Verdict Manual `_ for metrics and range definitions. """ +HANDLER: logging.Handler = VTKHandler() + @SISOFilter( category=FilterCategory.QC, decoratedLabel="Mesh Quality Enhanced", decoratedType="vtkUnstructuredGrid" ) class PVMeshQualityEnhanced( VTKPythonAlgorithmBase ): @@ -68,8 +70,6 @@ def __init__( self: Self ) -> None: self._saveToFile: bool = True self._blockIndex: int = 0 - self.handler: logging.Handler = VTKHandler() - # Used to concatenate results if vtkMultiBlockDataSet self._metricsAll: list[ float ] = [] @@ -232,8 +232,8 @@ def ApplyFilter( self, inputMesh: vtkUnstructuredGrid, outputMesh: vtkUnstructur otherMetrics: set[ int ] = self._getQualityMetricsToUse( self._commonMeshQualityMetric ) meshQualityEnhancedFilter: MeshQualityEnhanced = MeshQualityEnhanced( inputMesh, True ) - if not isHandlerInLogger( self.handler, meshQualityEnhancedFilter.logger ): - meshQualityEnhancedFilter.setLoggerHandler( self.handler ) + if not isHandlerInLogger( HANDLER, meshQualityEnhancedFilter.logger ): + meshQualityEnhancedFilter.setLoggerHandler( HANDLER ) meshQualityEnhancedFilter.SetCellQualityMetrics( triangleMetrics=triangleMetrics, quadMetrics=quadMetrics, diff --git a/geos-pv/src/geos/pv/utils/workflowFunctions.py b/geos-pv/src/geos/pv/utils/workflowFunctions.py index 1a66f1a7..88b6f6f8 100644 --- a/geos-pv/src/geos/pv/utils/workflowFunctions.py +++ b/geos-pv/src/geos/pv/utils/workflowFunctions.py @@ -9,8 +9,9 @@ from vtkmodules.vtkCommonDataModel import vtkMultiBlockDataSet -from paraview.detail.loghandler import ( VTKHandler ) # type: ignore[import-not-found] +from paraview.detail.loghandler import VTKHandler # type: ignore[import-not-found] +HANDLER: logging.Handler = VTKHandler() def doExtractAndMerge( mesh: vtkMultiBlockDataSet, @@ -37,9 +38,8 @@ def doExtractAndMerge( extractFault=extractFault, extractWell=extractWell, speHandler=True ) - handler: logging.Handler = VTKHandler() - if not isHandlerInLogger( handler, blockExtractor.logger ): - blockExtractor.setLoggerHandler( handler ) + if not isHandlerInLogger( HANDLER, blockExtractor.logger ): + blockExtractor.setLoggerHandler( HANDLER ) blockExtractor.applyFilter() # Add to the warning counter the number of warning logged with the call of GeosBlockExtractor filter @@ -84,9 +84,8 @@ def mergeBlocksFilter( """ loggerName = f"GEOS Block Merge for the domain { domainToMerge }" mergeBlockFilter: GeosBlockMerge = GeosBlockMerge( mesh, convertSurfaces, True, loggerName ) - handler: logging.Handler = VTKHandler() - if not isHandlerInLogger( handler, mergeBlockFilter.logger ): - mergeBlockFilter.setLoggerHandler( handler ) + if not isHandlerInLogger( HANDLER, mergeBlockFilter.logger ): + mergeBlockFilter.setLoggerHandler( HANDLER ) mergeBlockFilter.applyFilter() # Add to the warning counter the number of warning logged with the call of GeosBlockMerge filter diff --git a/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py b/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py new file mode 100644 index 00000000..4a2f242c --- /dev/null +++ b/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py @@ -0,0 +1,369 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. +"""Check that tagged 2D elements are internal (have exactly 2 volume neighbors).""" + +from dataclasses import dataclass +from typing import Optional, Dict, List, Union +import vtk +from tqdm import tqdm + +from geos.mesh_doctor.parsing.cliParsing import setupLogger +from geos.mesh.io.vtkIO import readUnstructuredGrid, writeMesh, VtkOutput + + +@dataclass( frozen=True ) +class Options: + """Options for the internal tags check. + + Attributes: + tagValues: List of tag values to check + tagArrayName: Name of the cell data array containing tags + outputCsv: Optional path to output CSV file with problematic elements + nullTagValue: Optional tag value to assign to faulty cells (e.g., 9999) + fixedVtkOutput: Optional VtkOutput for mesh with faulty cells retagged + verbose: Enable detailed connectivity diagnostics for problematic cells + """ + tagValues: tuple[ int, ...] + tagArrayName: str + outputCsv: Optional[ str ] + nullTagValue: Optional[ int ] + fixedVtkOutput: Optional[ VtkOutput ] + verbose: bool = False + + +@dataclass( frozen=True ) +class Result: + """Result of the internal tags check. + + Attributes: + info: Summary information about the check + passed: Whether all checked elements have exactly 2 neighbors + """ + info: str + passed: bool + + +@dataclass( frozen=True ) +class ElementInfo: + """Information about a tagged element. + + Attributes: + cellId: Cell ID in the mesh + tag: Tag value + numNeighbors: Number of volume neighbors + neighbors: List of neighbor cell IDs + """ + cellId: int + tag: int + numNeighbors: int + neighbors: list[ int ] + + +def __getVolumeNeighbors( mesh: vtk.vtkUnstructuredGrid, cellId: int, volumeCells: set[ int ], + surfaceCellTypes: list[ int ] ) -> list[ int ]: + """Find all volume neighbors of a 2D cell. + + Args: + mesh: The unstructured grid + cellId: ID of the 2D cell + volumeCells: Set of volume cell IDs + surfaceCellTypes: List of surface cell type IDs + + Returns: + List of volume cell IDs that share all points with the 2D cell + """ + surfaceCell = mesh.GetCell( cellId ) + + # Get all points of the surface cell + surfacePoints = set() + for i in range( surfaceCell.GetNumberOfPoints() ): + surfacePoints.add( surfaceCell.GetPointId( i ) ) + + # Get cells connected to the first point + firstPoint = surfaceCell.GetPointId( 0 ) + neighborCells = vtk.vtkIdList() + mesh.GetPointCells( firstPoint, neighborCells ) + + # Find volume cells that contain all surface points + volumeNeighbors = [] + for i in range( neighborCells.GetNumberOfIds() ): + neighborId = neighborCells.GetId( i ) + + # Skip if not a volume cell + if neighborId not in volumeCells: + continue + + # Get points of the volume cell + volumeCell = mesh.GetCell( neighborId ) + volumePoints = set() + for j in range( volumeCell.GetNumberOfPoints() ): + volumePoints.add( volumeCell.GetPointId( j ) ) + + # Check if all surface points are in the volume cell + if surfacePoints.issubset( volumePoints ): + volumeNeighbors.append( neighborId ) + + return volumeNeighbors + + +def __diagnose1NeighborCell( mesh: vtk.vtkUnstructuredGrid, elem: ElementInfo, volumeCells: set[ int ] ) -> None: + """Diagnose a cell with only 1 neighbor by showing face-by-face neighbors. + + Args: + mesh: The unstructured grid + elem: The problematic element info (must have exactly 1 neighbor) + volumeCells: Set of all volume cell IDs + """ + if elem.numNeighbors != 1: + return + + # Get the 2D surface cell we're trying to match + surfaceCell = mesh.GetCell( elem.cellId ) + surfacePointIds = surfaceCell.GetPointIds() + surfaceNodes = [ surfacePointIds.GetId( i ) for i in range( surfacePointIds.GetNumberOfIds() ) ] + + neighborCellId = elem.neighbors[ 0 ] + volumeCell = mesh.GetCell( neighborCellId ) + cellTypeName = volumeCell.GetClassName() + numFaces = volumeCell.GetNumberOfFaces() + + setupLogger.warning( f" Cell {elem.cellId} (tag={elem.tag}) has only 1 neighbor: cell {neighborCellId}" ) + setupLogger.warning( f" 2D element nodes: {surfaceNodes}" ) + setupLogger.warning( f" Cell type: {cellTypeName} ({numFaces} faces)" ) + setupLogger.warning( " Face-by-face neighbors:" ) + + # For each face of the volume cell, find the neighbor + for faceIdx in range( numFaces ): + face = volumeCell.GetFace( faceIdx ) + facePointIds = face.GetPointIds() + + # Find neighboring cells that share this face + neighborCells = vtk.vtkIdList() + mesh.GetCellNeighbors( neighborCellId, facePointIds, neighborCells ) + + # Filter to only volume cells + volumeNeighbors = [] + for i in range( neighborCells.GetNumberOfIds() ): + nId = neighborCells.GetId( i ) + if nId in volumeCells and nId != neighborCellId: + volumeNeighbors.append( nId ) + + if volumeNeighbors: + setupLogger.warning( f" Face {faceIdx}: {volumeNeighbors}" ) + else: + setupLogger.warning( f" Face {faceIdx}: " ) + + +def checkInternalTags( mesh: vtk.vtkUnstructuredGrid, options: Options ) -> Result: + """Check that all 2D elements with specified tags have exactly 2 volume neighbors. + + Args: + mesh: Input unstructured grid + options: Check options + + Returns: + Result with summary information + + Raises: + ValueError: If tag array not found or other validation errors + """ + setupLogger.info( f"Mesh: {mesh.GetNumberOfPoints()} points, {mesh.GetNumberOfCells()} cells" ) + + # Get tags array + cellData = mesh.GetCellData() + if not cellData.HasArray( options.tagArrayName ): + availableArrays = [ cellData.GetArrayName( i ) for i in range( cellData.GetNumberOfArrays() ) ] + raise ValueError( f"Tag array '{options.tagArrayName}' not found. " + f"Available cell data arrays: {availableArrays}" ) + + tags = cellData.GetArray( options.tagArrayName ) + + # Convert tag array to int if needed + if not isinstance( tags, vtk.vtkIntArray ): + setupLogger.info( f"Converting tag array '{options.tagArrayName}' to integer type..." ) + intTagsArray = vtk.vtkIntArray() + intTagsArray.SetName( tags.GetName() ) + intTagsArray.SetNumberOfComponents( tags.GetNumberOfComponents() ) + for i in range( tags.GetNumberOfTuples() ): + intTagsArray.InsertNextValue( int( tags.GetValue( i ) ) ) + mesh.GetCellData().RemoveArray( options.tagArrayName ) + mesh.GetCellData().AddArray( intTagsArray ) + tags = intTagsArray + + # Define cell types + SURFACE_CELL_TYPES = [ vtk.VTK_TRIANGLE, vtk.VTK_QUAD, vtk.VTK_POLYGON, vtk.VTK_PIXEL ] + VOLUME_CELL_TYPES = [ vtk.VTK_TETRA, vtk.VTK_HEXAHEDRON, vtk.VTK_WEDGE, vtk.VTK_PYRAMID, vtk.VTK_VOXEL ] + + # Build connectivity + setupLogger.info( "Building cell connectivity..." ) + mesh.BuildLinks() + + # Find volume cells and build tag mapping for 2D cells in one pass + nCells = mesh.GetNumberOfCells() + volumeCells = set() + tagToCells: Dict[ int, List[ int ] ] = {} # Map tag values to list of 2D cell IDs + + for cellId in tqdm( range( nCells ), desc="Building cell mappings" ): + cellType = mesh.GetCellType( cellId ) + + # Collect volume cells + if cellType in VOLUME_CELL_TYPES: + volumeCells.add( cellId ) + + # Collect 2D cells by tag (only for tags we're interested in) + if cellType in SURFACE_CELL_TYPES: + currentTag = tags.GetValue( cellId ) + if currentTag in options.tagValues: + if currentTag not in tagToCells: + tagToCells[ currentTag ] = [] + tagToCells[ currentTag ].append( cellId ) + + setupLogger.info( f"Found {len(volumeCells)} volume cells" ) + + # Store results by tag + tagResults = {} + allBadElements = [] + + # Process each tag value + for tagValue in options.tagValues: + setupLogger.info( f"{'='*60}" ) + setupLogger.info( f"Checking tag = {tagValue}" ) + setupLogger.info( f"{'='*60}" ) + + elementsByNeighbors: Dict[ Union[ int, str ], List[ ElementInfo ] ] = { 0: [], 1: [], 2: [], 'other': [] } + + # Get cells with this tag (pre-filtered) + cellsWithTag = tagToCells.get( tagValue, [] ) + setupLogger.info( f"Found {len(cellsWithTag)} cells with tag {tagValue}" ) + + # Process only the cells with this specific tag + for cellId in tqdm( cellsWithTag, desc=f"Processing tag {tagValue}" ): + # Count volume neighbors + volumeNeighbors = __getVolumeNeighbors( mesh, cellId, volumeCells, SURFACE_CELL_TYPES ) + numNeighbors = len( volumeNeighbors ) + + elemInfo = ElementInfo( cellId=cellId, tag=tagValue, numNeighbors=numNeighbors, neighbors=volumeNeighbors ) + + # Categorize by neighbor count + if numNeighbors in { 0, 1, 2 }: + elementsByNeighbors[ numNeighbors ].append( elemInfo ) + else: + elementsByNeighbors[ 'other' ].append( elemInfo ) + + # Calculate totals + total = sum( len( v ) for v in elementsByNeighbors.values() ) + + # Print summary for this tag + setupLogger.info( f"Summary for tag = {tagValue}:" ) + setupLogger.info( f" Total 2D cells: {total}" ) + setupLogger.info( f" With 0 neighbors: {len(elementsByNeighbors[0])} cells" ) + setupLogger.info( f" With 1 neighbor: {len(elementsByNeighbors[1])} cells" ) + setupLogger.info( f" With 2 neighbors: {len(elementsByNeighbors[2])} cells" ) + setupLogger.info( f" With 3+ neighbors: {len(elementsByNeighbors['other'])} cells" ) + + # Collect bad elements + allBadElements.extend( elementsByNeighbors[ 0 ] ) + allBadElements.extend( elementsByNeighbors[ 1 ] ) + allBadElements.extend( elementsByNeighbors[ 'other' ] ) + + tagResults[ tagValue ] = elementsByNeighbors + + # Print overall summary + setupLogger.info( f"{'='*60}" ) + setupLogger.info( "OVERALL SUMMARY" ) + setupLogger.info( f"{'='*60}" ) + + totalBad = len( allBadElements ) + totalChecked = sum( sum( len( v ) for v in neighbors.values() ) for neighbors in tagResults.values() ) + + setupLogger.info( f"Tags checked: {list(options.tagValues)}" ) + setupLogger.info( f"Total 2D cells checked: {totalChecked}" ) + setupLogger.info( f"Cells with exactly 2 neighbors: {totalChecked - totalBad}" ) + setupLogger.info( f"Cells with other than 2 neighbors: {totalBad}" ) + + if totalBad > 0: + setupLogger.warning( f"Found {totalBad} problematic cells across all tags!" ) + # Group bad elements by tag for reporting + badByTag = {} + for elem in allBadElements: + if elem.tag not in badByTag: + badByTag[ elem.tag ] = 0 + badByTag[ elem.tag ] += 1 + for tag, count in sorted( badByTag.items() ): + setupLogger.warning( f" Tag {tag}: {count} problematic cells" ) + + # Diagnose cells with only 1 neighbor (only in verbose mode) + if options.verbose: + oneNeighborCells = [ elem for elem in allBadElements if elem.numNeighbors == 1 ] + if oneNeighborCells: + setupLogger.warning( f"{'='*60}" ) + setupLogger.warning( "CONNECTIVITY ANALYSIS FOR 1-NEIGHBOR CELLS" ) + setupLogger.warning( f"{'='*60}" ) + + for elem in oneNeighborCells: + __diagnose1NeighborCell( mesh, elem, volumeCells ) + else: + setupLogger.info( "All cells have exactly 2 neighbors!" ) + + # Write to CSV if requested + if options.outputCsv and allBadElements: + setupLogger.info( f"Writing bad elements to: {options.outputCsv}" ) + with open( options.outputCsv, 'w' ) as f: + f.write( "cell_id,tag,num_neighbors,neighbor_1,neighbor_2\n" ) + for elem in allBadElements: + neighbor1 = elem.neighbors[ 0 ] if len( elem.neighbors ) > 0 else -1 + neighbor2 = elem.neighbors[ 1 ] if len( elem.neighbors ) > 1 else -1 + f.write( f"{elem.cellId},{elem.tag},{elem.numNeighbors},{neighbor1},{neighbor2}\n" ) + setupLogger.info( f"Written {len(allBadElements)} rows" ) + elif options.outputCsv and not allBadElements: + setupLogger.info( "No bad elements to write (all cells have 2 neighbors)" ) + + # Retag faulty cells and write fixed mesh if requested + if options.fixedVtkOutput and options.nullTagValue is not None and allBadElements: + setupLogger.info( f"Retagging {len(allBadElements)} faulty cells to tag {options.nullTagValue}..." ) + + # Get the tags array + tagsArray = mesh.GetCellData().GetArray( options.tagArrayName ) + + # Create set of bad cell IDs for fast lookup + badCellIds = { elem.cellId for elem in allBadElements } + + # Retag the problematic cells + numRetagged = 0 + for cellId in badCellIds: + tagsArray.SetValue( cellId, options.nullTagValue ) + numRetagged += 1 + + setupLogger.info( f"Retagged {numRetagged} cells" ) + + # Write the modified mesh (tag array already in int format) + writeMesh( mesh, options.fixedVtkOutput ) + setupLogger.info( f"Written fixed mesh to: {options.fixedVtkOutput.output}" ) + elif options.fixedVtkOutput and not allBadElements: + setupLogger.info( "No faulty cells to retag (all cells have 2 neighbors)" ) + elif options.fixedVtkOutput and options.nullTagValue is None: + setupLogger.warning( "Cannot write fixed mesh: --nullTagValue not provided (e.g., add --nullTagValue 9999)" ) + + # Create result message + if totalBad > 0: + resultMsg = f"FAILED: Found {totalBad} non-internal elements out of {totalChecked} checked" + passed = False + else: + resultMsg = f"PASSED: All {totalChecked} tagged elements are internal (have exactly 2 volume neighbors)" + passed = True + + return Result( info=resultMsg, passed=passed ) + + +def action( vtuInputFile: str, options: Options ) -> Result: + """Main action to check that tagged elements are internal. + + Args: + vtuInputFile: Path to input VTU file + options: Check options + + Returns: + Result with summary information + """ + mesh = readUnstructuredGrid( vtuInputFile ) + return checkInternalTags( mesh, options ) diff --git a/mesh-doctor/src/geos/mesh_doctor/parsing/__init__.py b/mesh-doctor/src/geos/mesh_doctor/parsing/__init__.py index 2aca9fcd..8c9a2e03 100644 --- a/mesh-doctor/src/geos/mesh_doctor/parsing/__init__.py +++ b/mesh-doctor/src/geos/mesh_doctor/parsing/__init__.py @@ -17,6 +17,7 @@ SELF_INTERSECTING_ELEMENTS = "selfIntersectingElements" SUPPORTED_ELEMENTS = "supportedElements" ORPHAN_2D = "orphan2d" +CHECK_INTERNAL_TAGS = "checkInternalTags" @dataclass( frozen=True ) diff --git a/mesh-doctor/src/geos/mesh_doctor/parsing/checkInternalTagsParsing.py b/mesh-doctor/src/geos/mesh_doctor/parsing/checkInternalTagsParsing.py new file mode 100644 index 00000000..fe47ed0b --- /dev/null +++ b/mesh-doctor/src/geos/mesh_doctor/parsing/checkInternalTagsParsing.py @@ -0,0 +1,124 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Command line parsing for internal tags check.""" + +from __future__ import annotations +import argparse +from typing import Any, Optional + +from geos.mesh_doctor.actions.checkInternalTags import Options, Result +from geos.mesh_doctor.parsing import CHECK_INTERNAL_TAGS +from geos.mesh_doctor.parsing.cliParsing import setupLogger, addVtuInputFileArgument +from geos.mesh.io.vtkIO import VtkOutput + +__TAG_VALUES = "tagValues" +__TAG_ARRAY = "tagArray" +__OUTPUT_CSV = "outputCsv" +__NULL_TAG_VALUE = "nullTagValue" +__FIXED_OUTPUT = "fixedOutput" +__VERBOSE = "verbose" + +__TAG_ARRAY_DEFAULT = "tags" + + +def convert( parsedOptions: dict[ str, Any ] ) -> Options: + """Convert parsed command-line options to Options object. + + Args: + parsedOptions: Dictionary of parsed command-line options. + + Returns: + Options: Configuration options for internal tags check. + """ + # Create VtkOutput for fixed mesh if specified (always binary mode) + fixedOutput: Optional[ str ] = parsedOptions.get( __FIXED_OUTPUT ) + fixedVtkOutput = None + if fixedOutput: + fixedVtkOutput = VtkOutput( output=fixedOutput, isDataModeBinary=True ) + + return Options( tagValues=tuple( parsedOptions[ __TAG_VALUES ] ), + tagArrayName=parsedOptions.get( __TAG_ARRAY, __TAG_ARRAY_DEFAULT ), + outputCsv=parsedOptions.get( __OUTPUT_CSV ), + nullTagValue=parsedOptions.get( __NULL_TAG_VALUE ), + fixedVtkOutput=fixedVtkOutput, + verbose=parsedOptions.get( __VERBOSE, False ) ) + + +def fillSubparser( subparsers: argparse._SubParsersAction[ Any ] ) -> None: + """Fill the argument parser for the checkInternalTags action. + + Args: + subparsers: Subparsers from the main argument parser + """ + p = subparsers.add_parser( CHECK_INTERNAL_TAGS, + help="Check that tagged 2D elements are internal (have exactly 2 volume neighbors).", + description="""\ +Validates that 2D elements with specified tag values have exactly 2 volume neighbors. +Elements with 0, 1, or 3+ neighbors are reported as problematic, as they indicate elements +on the mesh boundary or other geometric issues. + +This check helps ensure that tagged internal surfaces (e.g., fractures) are properly embedded +in the volume mesh and not inadvertently placed on external boundaries. +""" ) + + addVtuInputFileArgument( p ) + + p.add_argument( '--' + __TAG_VALUES, + nargs='+', + type=int, + required=True, + metavar='VALUE', + help="[ints]: Tag values to check (space-separated list, e.g., --tagValues 8 9 10)" ) + + p.add_argument( '--' + __TAG_ARRAY, + type=str, + default=__TAG_ARRAY_DEFAULT, + metavar='NAME', + help=f"[string]: Name of the cell data array containing tags. Defaults to '{__TAG_ARRAY_DEFAULT}'" ) + + p.add_argument( '--' + __OUTPUT_CSV, + type=str, + default=None, + metavar='FILE', + help="[string]: Output CSV file for problematic elements (optional)" ) + + p.add_argument( '--' + __NULL_TAG_VALUE, + type=int, + default=None, + metavar='VALUE', + help="[int]: Tag value to assign to faulty cells (e.g., 9999). Required to use --fixedOutput." ) + + p.add_argument( '--' + __FIXED_OUTPUT, + type=str, + default=None, + metavar='FILE', + help="[string]: Output VTU file with faulty cells retagged to nullTagValue (optional)" ) + + p.add_argument( '--' + __VERBOSE, + '-v', + action='store_true', + help="[flag]: Enable detailed connectivity diagnostics for problematic cells" ) + + +def displayResults( options: Options, result: Result ) -> None: + """Display the results of the internal tags check. + + Args: + options: The options used for the check. + result: The result of the check. + """ + setupLogger.results( "=" * 80 ) + setupLogger.results( "INTERNAL TAGS CHECK RESULTS" ) + setupLogger.results( "=" * 80 ) + setupLogger.results( result.info ) + setupLogger.results( "=" * 80 ) + + if "FAILED" in result.info: + setupLogger.results( "Validation FAILED: Some tagged elements are not internal" ) + if options.outputCsv: + setupLogger.results( f"See {options.outputCsv} for details on problematic elements" ) + if options.fixedVtkOutput and options.nullTagValue is not None: + setupLogger.results( + f"Fixed mesh written to {options.fixedVtkOutput.output} (faulty cells retagged to {options.nullTagValue})" + ) + else: + setupLogger.results( "Validation PASSED: All tagged elements are internal" ) diff --git a/mesh-doctor/src/geos/mesh_doctor/parsing/orphan2dParsing.py b/mesh-doctor/src/geos/mesh_doctor/parsing/orphan2dParsing.py index f8777aec..afdb277e 100644 --- a/mesh-doctor/src/geos/mesh_doctor/parsing/orphan2dParsing.py +++ b/mesh-doctor/src/geos/mesh_doctor/parsing/orphan2dParsing.py @@ -11,9 +11,6 @@ __ORPHAN_OUTPUT = "orphanOutput" __CLEAN_OUTPUT = "cleanOutput" -__DATA_MODE = "dataMode" -__DATA_MODE_VALUES = "binary", "ascii" -__DATA_MODE_DEFAULT = __DATA_MODE_VALUES[ 0 ] def convert( parsedOptions: dict[ str, Any ] ) -> Options: @@ -27,18 +24,16 @@ def convert( parsedOptions: dict[ str, Any ] ) -> Options: """ orphanOutput: Optional[ str ] = parsedOptions.get( __ORPHAN_OUTPUT ) cleanOutput: Optional[ str ] = parsedOptions.get( __CLEAN_OUTPUT ) - dataMode: str = parsedOptions.get( __DATA_MODE, __DATA_MODE_DEFAULT ) - isDataModeBinary: bool = dataMode == __DATA_MODE_DEFAULT - # Create VtkOutput for orphan file if specified + # Create VtkOutput for orphan file if specified (always binary mode) orphanVtkOutput = None if orphanOutput: - orphanVtkOutput = VtkOutput( output=orphanOutput, isDataModeBinary=isDataModeBinary ) + orphanVtkOutput = VtkOutput( output=orphanOutput, isDataModeBinary=True ) - # Create VtkOutput for clean file if specified + # Create VtkOutput for clean file if specified (always binary mode) cleanVtkOutput = None if cleanOutput: - cleanVtkOutput = VtkOutput( output=cleanOutput, isDataModeBinary=isDataModeBinary ) + cleanVtkOutput = VtkOutput( output=cleanOutput, isDataModeBinary=True ) return Options( orphanVtkOutput=orphanVtkOutput, cleanVtkOutput=cleanVtkOutput ) @@ -60,12 +55,6 @@ def fillSubparser( subparsers: _SubParsersAction[ Any ] ) -> None: type=str, metavar='FILE', help="[string]: Output VTU file with orphaned 2D cells removed." ) - p.add_argument( - '--' + __DATA_MODE, - type=str, - metavar=", ".join( __DATA_MODE_VALUES ), - default=__DATA_MODE_DEFAULT, - help='[string]: For ".vtu" output format, the data mode can be binary or ascii. Defaults to binary.' ) def displayResults( options: Options, result: Result ) -> None: diff --git a/mesh-doctor/src/geos/mesh_doctor/register.py b/mesh-doctor/src/geos/mesh_doctor/register.py index 3a39b705..04932204 100644 --- a/mesh-doctor/src/geos/mesh_doctor/register.py +++ b/mesh-doctor/src/geos/mesh_doctor/register.py @@ -58,7 +58,8 @@ def registerParsingActions( for actionName in ( parsing.ALL_CHECKS, parsing.COLLOCATES_NODES, parsing.ELEMENT_VOLUMES, parsing.FIX_ELEMENTS_ORDERINGS, parsing.GENERATE_CUBE, parsing.GENERATE_FRACTURES, parsing.GENERATE_GLOBAL_IDS, parsing.MAIN_CHECKS, parsing.NON_CONFORMAL, - parsing.SELF_INTERSECTING_ELEMENTS, parsing.SUPPORTED_ELEMENTS, parsing.ORPHAN_2D ): + parsing.SELF_INTERSECTING_ELEMENTS, parsing.SUPPORTED_ELEMENTS, parsing.ORPHAN_2D, + parsing.CHECK_INTERNAL_TAGS ): __HELPERS[ actionName ] = actionName __ACTIONS[ actionName ] = actionName