From 0202ea08e42d99f8dc6f32e0ea8de5a85710a038 Mon Sep 17 00:00:00 2001 From: DENEL Bertrand Date: Tue, 24 Feb 2026 17:44:50 -0600 Subject: [PATCH 1/7] Draft --- .../mesh_doctor/actions/checkInternalTags.py | 360 ++++++++++++++++++ .../src/geos/mesh_doctor/parsing/__init__.py | 5 + .../parsing/checkInternalTagsParsing.py | 138 +++++++ mesh-doctor/src/geos/mesh_doctor/register.py | 5 + 4 files changed, 508 insertions(+) create mode 100644 mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py create mode 100644 mesh-doctor/src/geos/mesh_doctor/parsing/checkInternalTagsParsing.py 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 000000000..23bc3530f --- /dev/null +++ b/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py @@ -0,0 +1,360 @@ +# 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 +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[ float, ...] + tagArrayName: str + outputCsv: Optional[ str ] + nullTagValue: Optional[ float ] + 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: float + 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 ) + + # 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 = {} # 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 = { 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 + for elem in elementsByNeighbors[ 0 ]: + allBadElements.append( elem ) + for elem in elementsByNeighbors[ 1 ]: + allBadElements.append( elem ) + for elem in elementsByNeighbors[ 'other' ]: + allBadElements.append( elem ) + + 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 + 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 7169fad39..46aa8fcf9 100644 --- a/mesh-doctor/src/geos/mesh_doctor/parsing/__init__.py +++ b/mesh-doctor/src/geos/mesh_doctor/parsing/__init__.py @@ -16,6 +16,11 @@ NON_CONFORMAL = "nonConformal" SELF_INTERSECTING_ELEMENTS = "selfIntersectingElements" SUPPORTED_ELEMENTS = "supportedElements" +<<<<<<< Updated upstream +======= +ORPHAN_2D = "orphan2d" +CHECK_INTERNAL_TAGS = "checkInternalTags" +>>>>>>> Stashed changes @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 000000000..7b9c6c72a --- /dev/null +++ b/mesh-doctor/src/geos/mesh_doctor/parsing/checkInternalTagsParsing.py @@ -0,0 +1,138 @@ +# 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" +__DATA_MODE = "dataMode" +__VERBOSE = "verbose" + +__TAG_ARRAY_DEFAULT = "tags" +__DATA_MODE_VALUES = "binary", "ascii" +__DATA_MODE_DEFAULT = __DATA_MODE_VALUES[ 0 ] + + +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. + """ + # Get dataMode setting + dataMode: str = parsedOptions.get( __DATA_MODE, __DATA_MODE_DEFAULT ) + isDataModeBinary: bool = dataMode == __DATA_MODE_DEFAULT + + # Create VtkOutput for fixed mesh if specified + fixedOutput: Optional[ str ] = parsedOptions.get( __FIXED_OUTPUT ) + fixedVtkOutput = None + if fixedOutput: + fixedVtkOutput = VtkOutput( output=fixedOutput, isDataModeBinary=isDataModeBinary ) + + 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=float, + required=True, + metavar='VALUE', + help="[floats]: 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=float, + default=None, + metavar='VALUE', + help="[float]: 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( + '--' + __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.' ) + + 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/register.py b/mesh-doctor/src/geos/mesh_doctor/register.py index bb677f4ee..7f2d8c2f6 100644 --- a/mesh-doctor/src/geos/mesh_doctor/register.py +++ b/mesh-doctor/src/geos/mesh_doctor/register.py @@ -58,7 +58,12 @@ 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, +<<<<<<< Updated upstream parsing.SELF_INTERSECTING_ELEMENTS, parsing.SUPPORTED_ELEMENTS ): +======= + parsing.SELF_INTERSECTING_ELEMENTS, parsing.SUPPORTED_ELEMENTS, parsing.ORPHAN_2D, + parsing.CHECK_INTERNAL_TAGS ): +>>>>>>> Stashed changes __HELPERS[ actionName ] = actionName __ACTIONS[ actionName ] = actionName From bdea537a0d6102235ec38ea45471f2489e1b7237 Mon Sep 17 00:00:00 2001 From: Bertrand Denel <120652669+bd713@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:41:06 -0800 Subject: [PATCH 2/7] Apply suggestions from code review Co-authored-by: Jacques Franc <49998870+jafranc@users.noreply.github.com> --- .../src/geos/mesh_doctor/actions/checkInternalTags.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py b/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py index 23bc3530f..5543e5ce4 100644 --- a/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py +++ b/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py @@ -3,7 +3,7 @@ """Check that tagged 2D elements are internal (have exactly 2 volume neighbors).""" from dataclasses import dataclass -from typing import Optional +from typing import Optional, Dict, List, Any import vtk from tqdm import tqdm @@ -189,7 +189,7 @@ def checkInternalTags( mesh: vtk.vtkUnstructuredGrid, options: Options ) -> Resu # Find volume cells and build tag mapping for 2D cells in one pass nCells = mesh.GetNumberOfCells() volumeCells = set() - tagToCells = {} # Map tag values to list of 2D cell IDs + 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 ) @@ -218,7 +218,7 @@ def checkInternalTags( mesh: vtk.vtkUnstructuredGrid, options: Options ) -> Resu setupLogger.info( f"Checking tag = {tagValue}" ) setupLogger.info( f"{'='*60}" ) - elementsByNeighbors = { 0: [], 1: [], 2: [], 'other': [] } + elementsByNeighbors: Dict[Any, List[int]] = { 0: [], 1: [], 2: [], 'other': [] } # Get cells with this tag (pre-filtered) cellsWithTag = tagToCells.get( tagValue, [] ) From 5103bdf17265f7464595b480bf0dfa54535b84fb Mon Sep 17 00:00:00 2001 From: Romain Baville <126683264+RomainBaville@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:19:37 +0100 Subject: [PATCH 3/7] fix: Fix VTKHandler managment in geos-pv (#234) --- .../generic_processing_tools/SplitMesh.py | 7 +++++++ .../pre_processing/MeshQualityEnhanced.py | 8 ++++++++ .../generic_processing/PVAttributeMapping.py | 7 ++++--- .../plugins/generic_processing/PVClipToMainFrame.py | 7 ++++--- .../PVCreateConstantAttributePerRegion.py | 7 ++++--- .../generic_processing/PVFillPartialArrays.py | 7 ++++--- .../generic_processing/PVMergeBlocksEnhanced.py | 7 ++++--- .../pv/plugins/generic_processing/PVSplitMesh.py | 7 ++++--- .../post_processing/PVGeomechanicsCalculator.py | 13 +++++++------ .../post_processing/PVGeomechanicsWorkflow.py | 4 ++-- .../post_processing/PVGeosBlockExtractAndMerge.py | 4 ++-- .../pv/plugins/post_processing/PVGeosLogReader.py | 4 ++-- .../pv/plugins/post_processing/PVMohrCirclePlot.py | 4 ++-- .../post_processing/PVSurfaceGeomechanics.py | 8 ++++---- .../geos/pv/plugins/qc/PVCellTypeCounterEnhanced.py | 8 ++++---- .../src/geos/pv/plugins/qc/PVMeshQualityEnhanced.py | 8 ++++---- geos-pv/src/geos/pv/utils/workflowFunctions.py | 13 ++++++------- 17 files changed, 72 insertions(+), 51 deletions(-) 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 2695c8f85..2d917027f 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 0e75daffc..3f75cfae2 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 d6b07cab6..20c29a0be 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 4c04589ae..3461b134b 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 372ea2c3a..bb8973aa0 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 547945aab..4a6e27d2d 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 81da5b814..7a3b15d5d 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 693a5f5f0..0f7072561 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 0b446062e..efb1c089a 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 dff27de74..62e415a87 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 4b8aea9f6..b46fd4638 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 e3ff25853..625ac5c3f 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 2cbc5ba22..f63c2b82d 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 44e99ced4..c46c429a3 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 d959b1e89..2c00aadca 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 1a66f1a78..88b6f6f85 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 From a5eeaed604dc285e37d848f4f87829bb263b2e48 Mon Sep 17 00:00:00 2001 From: DENEL Bertrand Date: Fri, 27 Feb 2026 19:26:19 -0600 Subject: [PATCH 4/7] Changed tag to int, removed ascii option --- .../mesh_doctor/actions/checkInternalTags.py | 29 ++++++++++++------- .../parsing/checkInternalTagsParsing.py | 26 ++++------------- .../mesh_doctor/parsing/orphan2dParsing.py | 19 +++--------- 3 files changed, 29 insertions(+), 45 deletions(-) diff --git a/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py b/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py index 5543e5ce4..bf9827e59 100644 --- a/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py +++ b/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py @@ -23,10 +23,10 @@ class Options: fixedVtkOutput: Optional VtkOutput for mesh with faulty cells retagged verbose: Enable detailed connectivity diagnostics for problematic cells """ - tagValues: tuple[ float, ...] + tagValues: tuple[ int, ...] tagArrayName: str outputCsv: Optional[ str ] - nullTagValue: Optional[ float ] + nullTagValue: Optional[ int ] fixedVtkOutput: Optional[ VtkOutput ] verbose: bool = False @@ -54,7 +54,7 @@ class ElementInfo: neighbors: List of neighbor cell IDs """ cellId: int - tag: float + tag: int numNeighbors: int neighbors: list[ int ] @@ -178,6 +178,18 @@ def checkInternalTags( mesh: vtk.vtkUnstructuredGrid, options: Options ) -> Resu 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 ] @@ -250,12 +262,9 @@ def checkInternalTags( mesh: vtk.vtkUnstructuredGrid, options: Options ) -> Resu setupLogger.info( f" With 3+ neighbors: {len(elementsByNeighbors['other'])} cells" ) # Collect bad elements - for elem in elementsByNeighbors[ 0 ]: - allBadElements.append( elem ) - for elem in elementsByNeighbors[ 1 ]: - allBadElements.append( elem ) - for elem in elementsByNeighbors[ 'other' ]: - allBadElements.append( elem ) + allBadElements.extend( elementsByNeighbors[ 0 ] ) + allBadElements.extend( elementsByNeighbors[ 1 ] ) + allBadElements.extend( elementsByNeighbors[ 'other' ] ) tagResults[ tagValue ] = elementsByNeighbors @@ -327,7 +336,7 @@ def checkInternalTags( mesh: vtk.vtkUnstructuredGrid, options: Options ) -> Resu setupLogger.info( f"Retagged {numRetagged} cells" ) - # Write the modified mesh + # 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: diff --git a/mesh-doctor/src/geos/mesh_doctor/parsing/checkInternalTagsParsing.py b/mesh-doctor/src/geos/mesh_doctor/parsing/checkInternalTagsParsing.py index 7b9c6c72a..fe47ed0b4 100644 --- a/mesh-doctor/src/geos/mesh_doctor/parsing/checkInternalTagsParsing.py +++ b/mesh-doctor/src/geos/mesh_doctor/parsing/checkInternalTagsParsing.py @@ -15,12 +15,9 @@ __OUTPUT_CSV = "outputCsv" __NULL_TAG_VALUE = "nullTagValue" __FIXED_OUTPUT = "fixedOutput" -__DATA_MODE = "dataMode" __VERBOSE = "verbose" __TAG_ARRAY_DEFAULT = "tags" -__DATA_MODE_VALUES = "binary", "ascii" -__DATA_MODE_DEFAULT = __DATA_MODE_VALUES[ 0 ] def convert( parsedOptions: dict[ str, Any ] ) -> Options: @@ -32,15 +29,11 @@ def convert( parsedOptions: dict[ str, Any ] ) -> Options: Returns: Options: Configuration options for internal tags check. """ - # Get dataMode setting - dataMode: str = parsedOptions.get( __DATA_MODE, __DATA_MODE_DEFAULT ) - isDataModeBinary: bool = dataMode == __DATA_MODE_DEFAULT - - # Create VtkOutput for fixed mesh if specified + # 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=isDataModeBinary ) + fixedVtkOutput = VtkOutput( output=fixedOutput, isDataModeBinary=True ) return Options( tagValues=tuple( parsedOptions[ __TAG_VALUES ] ), tagArrayName=parsedOptions.get( __TAG_ARRAY, __TAG_ARRAY_DEFAULT ), @@ -71,10 +64,10 @@ def fillSubparser( subparsers: argparse._SubParsersAction[ Any ] ) -> None: p.add_argument( '--' + __TAG_VALUES, nargs='+', - type=float, + type=int, required=True, metavar='VALUE', - help="[floats]: Tag values to check (space-separated list, e.g., --tagValues 8 9 10)" ) + help="[ints]: Tag values to check (space-separated list, e.g., --tagValues 8 9 10)" ) p.add_argument( '--' + __TAG_ARRAY, type=str, @@ -89,10 +82,10 @@ def fillSubparser( subparsers: argparse._SubParsersAction[ Any ] ) -> None: help="[string]: Output CSV file for problematic elements (optional)" ) p.add_argument( '--' + __NULL_TAG_VALUE, - type=float, + type=int, default=None, metavar='VALUE', - help="[float]: Tag value to assign to faulty cells (e.g., 9999). Required to use --fixedOutput." ) + help="[int]: Tag value to assign to faulty cells (e.g., 9999). Required to use --fixedOutput." ) p.add_argument( '--' + __FIXED_OUTPUT, type=str, @@ -100,13 +93,6 @@ def fillSubparser( subparsers: argparse._SubParsersAction[ Any ] ) -> None: metavar='FILE', help="[string]: Output VTU file with faulty cells retagged to nullTagValue (optional)" ) - 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.' ) - p.add_argument( '--' + __VERBOSE, '-v', action='store_true', diff --git a/mesh-doctor/src/geos/mesh_doctor/parsing/orphan2dParsing.py b/mesh-doctor/src/geos/mesh_doctor/parsing/orphan2dParsing.py index f8777aecd..afdb277ef 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: From f4e0f0e72893ccae7bbf0a2c49e040bbd822db56 Mon Sep 17 00:00:00 2001 From: DENEL Bertrand Date: Fri, 27 Feb 2026 19:47:44 -0600 Subject: [PATCH 5/7] yapf --- mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py b/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py index bf9827e59..031b5cf55 100644 --- a/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py +++ b/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py @@ -201,7 +201,7 @@ def checkInternalTags( mesh: vtk.vtkUnstructuredGrid, options: Options ) -> Resu # 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 + 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 ) @@ -230,7 +230,7 @@ def checkInternalTags( mesh: vtk.vtkUnstructuredGrid, options: Options ) -> Resu setupLogger.info( f"Checking tag = {tagValue}" ) setupLogger.info( f"{'='*60}" ) - elementsByNeighbors: Dict[Any, List[int]] = { 0: [], 1: [], 2: [], 'other': [] } + elementsByNeighbors: Dict[ Any, List[ int ] ] = { 0: [], 1: [], 2: [], 'other': [] } # Get cells with this tag (pre-filtered) cellsWithTag = tagToCells.get( tagValue, [] ) From ff1725e43405f196acbba310696c28518688adf8 Mon Sep 17 00:00:00 2001 From: DENEL Bertrand Date: Fri, 27 Feb 2026 20:03:47 -0600 Subject: [PATCH 6/7] mypy --- mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py b/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py index 031b5cf55..30978e6c9 100644 --- a/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py +++ b/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py @@ -3,7 +3,7 @@ """Check that tagged 2D elements are internal (have exactly 2 volume neighbors).""" from dataclasses import dataclass -from typing import Optional, Dict, List, Any +from typing import Optional, Dict, List, Any, Union import vtk from tqdm import tqdm @@ -230,7 +230,7 @@ def checkInternalTags( mesh: vtk.vtkUnstructuredGrid, options: Options ) -> Resu setupLogger.info( f"Checking tag = {tagValue}" ) setupLogger.info( f"{'='*60}" ) - elementsByNeighbors: Dict[ Any, List[ int ] ] = { 0: [], 1: [], 2: [], 'other': [] } + elementsByNeighbors: Dict[Union[int, str], List[ElementInfo]] = { 0: [], 1: [], 2: [], 'other': [] } # Get cells with this tag (pre-filtered) cellsWithTag = tagToCells.get( tagValue, [] ) From daaa8c4d7954c0098bce848228983146a7fbe81f Mon Sep 17 00:00:00 2001 From: DENEL Bertrand Date: Fri, 27 Feb 2026 20:14:43 -0600 Subject: [PATCH 7/7] ruff --- mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py b/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py index 30978e6c9..4a2f242c5 100644 --- a/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py +++ b/mesh-doctor/src/geos/mesh_doctor/actions/checkInternalTags.py @@ -3,7 +3,7 @@ """Check that tagged 2D elements are internal (have exactly 2 volume neighbors).""" from dataclasses import dataclass -from typing import Optional, Dict, List, Any, Union +from typing import Optional, Dict, List, Union import vtk from tqdm import tqdm @@ -230,7 +230,7 @@ def checkInternalTags( mesh: vtk.vtkUnstructuredGrid, options: Options ) -> Resu setupLogger.info( f"Checking tag = {tagValue}" ) setupLogger.info( f"{'='*60}" ) - elementsByNeighbors: Dict[Union[int, str], List[ElementInfo]] = { 0: [], 1: [], 2: [], 'other': [] } + elementsByNeighbors: Dict[ Union[ int, str ], List[ ElementInfo ] ] = { 0: [], 1: [], 2: [], 'other': [] } # Get cells with this tag (pre-filtered) cellsWithTag = tagToCells.get( tagValue, [] )