From 5b9a80aa52a571f08d3bed69796a27d5b6f51fd9 Mon Sep 17 00:00:00 2001 From: DENEL Bertrand Date: Fri, 1 May 2026 14:20:02 -0500 Subject: [PATCH 1/4] Draft --- .../mesh_doctor/actions/generateFractures.py | 69 +++++++++++++++---- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/mesh-doctor/src/geos/mesh_doctor/actions/generateFractures.py b/mesh-doctor/src/geos/mesh_doctor/actions/generateFractures.py index 58a8385e..b51f80a4 100644 --- a/mesh-doctor/src/geos/mesh_doctor/actions/generateFractures.py +++ b/mesh-doctor/src/geos/mesh_doctor/actions/generateFractures.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from enum import Enum import networkx -from numpy import empty, ones, zeros +from numpy import empty, ones from tqdm import tqdm from typing import Collection, Iterable, Mapping, Optional, Sequence, TypeAlias from vtk import vtkDataArray @@ -516,6 +516,50 @@ def __performSplit( oldMesh: vtkUnstructuredGrid, cellToNodeMapping: Mapping[ in return newMesh +def __faceIsActuallySplit( oldMesh: vtkUnstructuredGrid, + cellToNodeMapping: Mapping[ int, IDMapping ], + ns: Collection[ int ], + pointIdsList: vtkIdList, + neighbors: vtkIdList ) -> bool: + """Tells whether the matrix will actually be split along the face whose vertices are ``ns``. + + A candidate fracture face is a real fracture surface only if the split + algorithm ends up assigning *different* node copies to its two adjacent 3D + cells for at least one of its vertices. If every vertex resolves to the + same copy in both cells the matrix is not separated along this face. + This includes fully-tip faces (no node duplicated anywhere) and intersection + artifacts (duplication exists but is for another fracture, and both + adjacent cells fell on the same side of that other fracture). + + Args: + oldMesh (vtkUnstructuredGrid): The input (pre-split) 3D mesh. + cellToNodeMapping (Mapping[ int, IDMapping ]): For each input cell, the per-vertex + remap from original to split-time node ids. + ns (Collection[ int ]): The face's vertex ids in the original mesh. + pointIdsList (vtkIdList): Scratch buffer for the face's vertex ids (reused across calls). + neighbors (vtkIdList): Scratch buffer for the adjacent-cells query (reused across calls). + + Returns: + bool: ``True`` if the face is a true fracture surface (kept), ``False`` if it should be discarded. + """ + pointIdsList.Reset() + for n in ns: + pointIdsList.InsertNextId( n ) + neighbors.Reset() + oldMesh.GetCellNeighbors( -1, pointIdsList, neighbors ) + adjacentCells = [ neighbors.GetId( i ) for i in range( neighbors.GetNumberOfIds() ) + if oldMesh.GetCell( neighbors.GetId( i ) ).GetCellDimension() == 3 ] + if len( adjacentCells ) < 2: + # Boundary face with only one adjacent 3D cell => keep it. + return True + for n in ns: + copyA = cellToNodeMapping.get( adjacentCells[ 0 ], {} ).get( n, n ) + copyB = cellToNodeMapping.get( adjacentCells[ 1 ], {} ).get( n, n ) + if copyA != copyB: + return True + return False + + def __generateFractureMesh( oldMesh: vtkUnstructuredGrid, fractureInfo: FractureInfo, cellToNodeMapping: Mapping[ int, IDMapping ] ) -> vtkUnstructuredGrid: """Generates the mesh of the fracture. @@ -532,35 +576,33 @@ def __generateFractureMesh( oldMesh: vtkUnstructuredGrid, fractureInfo: Fracture setupLogger.info( "Generating the meshes" ) meshPoints: vtkPoints = oldMesh.GetPoints() - isNodeDuplicated = zeros( meshPoints.GetNumberOfPoints(), dtype=bool ) # defaults to False - for nodeMapping in cellToNodeMapping.values(): - for i, o in nodeMapping.items(): - if not isNodeDuplicated[ i ]: - isNodeDuplicated[ i ] = i != o - # Some elements can have all their nodes not duplicated. - # In this case, it's mandatory not get rid of this element because the neighboring 3d elements won't follow. + # Filter out candidate faces that don't correspond to real fracture surfaces. + # See ``__faceIsActuallySplit`` for the criterion. + pointIdsList = vtkIdList() + neighbors = vtkIdList() faceNodes: list[ Collection[ int ] ] = [] discardedFaceNodes: set[ Iterable[ int ] ] = set() if fractureInfo.faceCellId != []: # The fracture policy is 'internalSurfaces' faceCellId: list[ int ] = [] for ns, fId in zip( fractureInfo.faceNodes, fractureInfo.faceCellId ): - if any( map( isNodeDuplicated.__getitem__, ns ) ): + if __faceIsActuallySplit( oldMesh, cellToNodeMapping, ns, pointIdsList, neighbors ): faceNodes.append( ns ) faceCellId.append( fId ) else: discardedFaceNodes.add( ns ) else: # The fracture policy is 'field' for ns in fractureInfo.faceNodes: - if any( map( isNodeDuplicated.__getitem__, ns ) ): + if __faceIsActuallySplit( oldMesh, cellToNodeMapping, ns, pointIdsList, neighbors ): faceNodes.append( ns ) else: discardedFaceNodes.add( ns ) if discardedFaceNodes: msg: str = "(" + '), ('.join( ", ".join( map( str, dfns ) ) for dfns in discardedFaceNodes ) + ")" - setupLogger.info( f"The faces made of nodes [{msg}] were/was discarded" - " from the fracture mesh because none of their/its nodes were duplicated." ) + setupLogger.info( f"The faces made of nodes [{msg}] were/was discarded from the fracture mesh" + " because the matrix is not actually split along them" + " (no node duplicated, or duplication is for another fracture)." ) fractureNodesTmp = ones( meshPoints.GetNumberOfPoints(), dtype=int ) * -1 for ns in faceNodes: @@ -640,7 +682,8 @@ def __splitMeshOnFractures( mesh: vtkUnstructuredGrid, outputMesh: vtkUnstructuredGrid = __performSplit( mesh, cellToNodeMapping ) fractureMeshes: list[ vtkUnstructuredGrid ] = [] for fractureInfoSeparated in allFractureInfos: - fractureMesh: vtkUnstructuredGrid = __generateFractureMesh( mesh, fractureInfoSeparated, cellToNodeMapping ) + fractureMesh: vtkUnstructuredGrid = __generateFractureMesh( mesh, fractureInfoSeparated, + cellToNodeMapping ) fractureMeshes.append( fractureMesh ) return ( outputMesh, fractureMeshes ) From eae23676bb096df2d4f73ebd9aeffb45f850aaae Mon Sep 17 00:00:00 2001 From: DENEL Bertrand Date: Fri, 1 May 2026 17:42:16 -0500 Subject: [PATCH 2/4] yapf --- .../mesh_doctor/actions/generateFractures.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/mesh-doctor/src/geos/mesh_doctor/actions/generateFractures.py b/mesh-doctor/src/geos/mesh_doctor/actions/generateFractures.py index b51f80a4..97d94ea6 100644 --- a/mesh-doctor/src/geos/mesh_doctor/actions/generateFractures.py +++ b/mesh-doctor/src/geos/mesh_doctor/actions/generateFractures.py @@ -516,11 +516,8 @@ def __performSplit( oldMesh: vtkUnstructuredGrid, cellToNodeMapping: Mapping[ in return newMesh -def __faceIsActuallySplit( oldMesh: vtkUnstructuredGrid, - cellToNodeMapping: Mapping[ int, IDMapping ], - ns: Collection[ int ], - pointIdsList: vtkIdList, - neighbors: vtkIdList ) -> bool: +def __faceIsActuallySplit( oldMesh: vtkUnstructuredGrid, cellToNodeMapping: Mapping[ int, IDMapping ], + ns: Collection[ int ], pointIdsList: vtkIdList, neighbors: vtkIdList ) -> bool: """Tells whether the matrix will actually be split along the face whose vertices are ``ns``. A candidate fracture face is a real fracture surface only if the split @@ -547,8 +544,10 @@ def __faceIsActuallySplit( oldMesh: vtkUnstructuredGrid, pointIdsList.InsertNextId( n ) neighbors.Reset() oldMesh.GetCellNeighbors( -1, pointIdsList, neighbors ) - adjacentCells = [ neighbors.GetId( i ) for i in range( neighbors.GetNumberOfIds() ) - if oldMesh.GetCell( neighbors.GetId( i ) ).GetCellDimension() == 3 ] + adjacentCells = [ + neighbors.GetId( i ) for i in range( neighbors.GetNumberOfIds() ) + if oldMesh.GetCell( neighbors.GetId( i ) ).GetCellDimension() == 3 + ] if len( adjacentCells ) < 2: # Boundary face with only one adjacent 3D cell => keep it. return True @@ -682,8 +681,7 @@ def __splitMeshOnFractures( mesh: vtkUnstructuredGrid, outputMesh: vtkUnstructuredGrid = __performSplit( mesh, cellToNodeMapping ) fractureMeshes: list[ vtkUnstructuredGrid ] = [] for fractureInfoSeparated in allFractureInfos: - fractureMesh: vtkUnstructuredGrid = __generateFractureMesh( mesh, fractureInfoSeparated, - cellToNodeMapping ) + fractureMesh: vtkUnstructuredGrid = __generateFractureMesh( mesh, fractureInfoSeparated, cellToNodeMapping ) fractureMeshes.append( fractureMesh ) return ( outputMesh, fractureMeshes ) From 0476e4472cb98e589c0f5d8c9744f4b27e8ae9cb Mon Sep 17 00:00:00 2001 From: Jacques Franc <49998870+jafranc@users.noreply.github.com> Date: Mon, 4 May 2026 18:01:50 +0200 Subject: [PATCH 3/4] Update file contributor and retrigger CI --- mesh-doctor/src/geos/mesh_doctor/actions/generateFractures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesh-doctor/src/geos/mesh_doctor/actions/generateFractures.py b/mesh-doctor/src/geos/mesh_doctor/actions/generateFractures.py index 97d94ea6..48aa2f9d 100644 --- a/mesh-doctor/src/geos/mesh_doctor/actions/generateFractures.py +++ b/mesh-doctor/src/geos/mesh_doctor/actions/generateFractures.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. -# SPDX-FileContributor: Thomas Gazolla, Alexandre Benedicto +# SPDX-FileContributor: Thomas Gazolla, Alexandre Benedicto, Bertrand Denel from collections import defaultdict from dataclasses import dataclass from enum import Enum From ad38f102726371463f89636caaec7afad577b2de Mon Sep 17 00:00:00 2001 From: Jacques Franc <49998870+jafranc@users.noreply.github.com> Date: Tue, 5 May 2026 10:29:53 +0200 Subject: [PATCH 4/4] Update GitHub Actions runner to ubuntu-22.04 --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 4db9621c..54d08e11 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -17,7 +17,7 @@ jobs: pull-requests: write # for amannn/action-semantic-pull-request to analyze PRs and statuses: write # for amannn/action-semantic-pull-request to mark status of analyzed PR contents: read - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Check if the PR name has conventional semantics