Skip to content

Commit a458b6e

Browse files
bd713DENEL BertrandOmarDurandkachumacastelletto1
authored
fix: Fracture/3D cell co-location in parallel mesh redistribution (#4008)
* Draft * working version * Clean * Clean * Fixed map * Clean * gpu * Clean Omar * JF Review * Missing protection * stdContainer 1 * stdContainer 2 * stdContainer 3 * Added schema * rebaseline --------- Co-authored-by: DENEL Bertrand <bertrand.denel@total.com> Co-authored-by: Omar Duran <oduran@stanford.edu> Co-authored-by: Dickson Kachuma <81433670+dkachuma@users.noreply.github.com> Co-authored-by: Nicola Castelletto <38361926+castelletto1@users.noreply.github.com>
1 parent 08bd4c6 commit a458b6e

16 files changed

Lines changed: 2197 additions & 312 deletions

.integrated_tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
baselines:
22
bucket: geosx
3-
baseline: integratedTests/baseline_integratedTests-pr4055-16683-0251bb6
3+
baseline: integratedTests/baseline_integratedTests-pr4008-16688-17c55fe
44

55
allow_fail:
66
all: ''

BASELINE_NOTES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ This file is designed to track changes to the integrated test baselines.
55
Any developer who updates the baseline ID in the .integrated_tests.yaml file is expected to create an entry in this file with the pull request number, date, and their justification for rebaselining.
66
These notes should be in reverse-chronological order, and use the following time format: (YYYY-MM-DD).
77

8+
PR #4008 (2026-05-18) <https://storage.googleapis.com/geosx/integratedTests/baseline_integratedTests-pr4008-16688-17c55fe.tar.gz>
9+
Fix Fracture/3D cell co-location in parallel mesh redistribution
10+
811
PR #4055 (2026-05-18) <https://storage.googleapis.com/geosx/integratedTests/baseline_integratedTests-pr4055-16683-0251bb6.tar.gz>
912
Trim some fluid model tests
1013

src/coreComponents/integrationTests/meshTests/testVTKImport.cpp

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,22 @@ using namespace geos::dataRepository;
5353
template< class V >
5454
void TestMeshImport( string const & meshFilePath, V const & validate, string const fractureName="" )
5555
{
56+
// Automatically use global IDs when fractures are present
57+
string const useGlobalIdsStr = fractureName.empty() ? "0" : "1";
58+
5659
string const pattern = R"xml(
5760
<Mesh>
5861
<VTKMesh
5962
name="mesh"
6063
file="{}"
6164
partitionRefinement="0"
62-
useGlobalIds="0"
65+
useGlobalIds="{}"
6366
{} />
6467
</Mesh>
6568
)xml";
66-
string const meshNode = GEOS_FMT( pattern, meshFilePath, fractureName.empty() ? "" : "faceBlocks=\"{" + fractureName + "}\"" );
69+
string const meshNode = GEOS_FMT( pattern, meshFilePath, useGlobalIdsStr,
70+
fractureName.empty() ? "" : "faceBlocks=\"{" + fractureName + "}\"" );
71+
6772
xmlWrapper::xmlDocument xmlDocument;
6873
xmlDocument.loadString( meshNode );
6974
xmlWrapper::xmlNode xmlMeshNode = xmlDocument.getChild( "Mesh" );
@@ -148,11 +153,12 @@ class TestFractureImport : public ::testing::Test
148153

149154
static std::filesystem::path createFractureMesh( std::filesystem::path const & folder )
150155
{
151-
// The main mesh
156+
// The main mesh - 3 hexahedra
152157
vtkNew< vtkUnstructuredGrid > main;
153158
{
154-
int constexpr numPoints = 16;
159+
int constexpr numPoints = 24; // 3 hexahedra * 8 points each
155160
double const pointsCoords[numPoints][3] = {
161+
// First hexahedron (x: -1 to 0, y: 0 to 1)
156162
{ -1, 0, 0 },
157163
{ -1, 1, 0 },
158164
{ -1, 1, 1 },
@@ -161,14 +167,26 @@ class TestFractureImport : public ::testing::Test
161167
{ 0, 1, 0 },
162168
{ 0, 1, 1 },
163169
{ 0, 0, 1 },
170+
// Second hexahedron (x: 0 to 1, y: 0 to 1) - shares face with first via fracture
164171
{ 0, 0, 0 },
165172
{ 0, 1, 0 },
166173
{ 0, 1, 1 },
167174
{ 0, 0, 1 },
168175
{ 1, 0, 0 },
169176
{ 1, 1, 0 },
170177
{ 1, 1, 1 },
171-
{ 1, 0, 1 } };
178+
{ 1, 0, 1 },
179+
// Third hexahedron (x: -1 to 0, y: 2 to 3) - disconnected from first two
180+
{ -1, 2, 0 },
181+
{ -1, 3, 0 },
182+
{ -1, 3, 1 },
183+
{ -1, 2, 1 },
184+
{ 0, 2, 0 },
185+
{ 0, 3, 0 },
186+
{ 0, 3, 1 },
187+
{ 0, 2, 1 }
188+
};
189+
172190
vtkNew< vtkPoints > points;
173191
points->Allocate( numPoints );
174192
for( double const * pointsCoord: pointsCoords )
@@ -177,15 +195,17 @@ class TestFractureImport : public ::testing::Test
177195
}
178196
main->SetPoints( points );
179197

180-
int constexpr numHexs = 2;
181-
vtkIdType const cubes[numHexs][8] = {
182-
{ 0, 1, 2, 3, 4, 5, 6, 7 },
183-
{ 8, 9, 10, 11, 12, 13, 14, 15 }
198+
int constexpr numHexs = 3;
199+
int constexpr pointsPerHex = 8;
200+
vtkIdType const cubes[numHexs][pointsPerHex] = {
201+
{ 0, 1, 2, 3, 4, 5, 6, 7 }, // Hex 0
202+
{ 8, 9, 10, 11, 12, 13, 14, 15 }, // Hex 1
203+
{ 16, 17, 18, 19, 20, 21, 22, 23 } // Hex 2
184204
};
185205
main->Allocate( numHexs );
186206
for( vtkIdType const * cube: cubes )
187207
{
188-
main->InsertNextCell( VTK_HEXAHEDRON, 8, cube );
208+
main->InsertNextCell( VTK_HEXAHEDRON, pointsPerHex, cube );
189209
}
190210

191211
vtkNew< vtkIdTypeArray > cellGlobalIds;
@@ -207,15 +227,17 @@ class TestFractureImport : public ::testing::Test
207227
main->GetPointData()->SetGlobalIds( pointGlobalIds );
208228
}
209229

210-
// The fracture mesh
230+
// The fracture mesh - 1 fracture connecting only hex 0 and hex 1
211231
vtkNew< vtkUnstructuredGrid > fracture;
212232
{
213233
int constexpr numPoints = 4;
214234
double const pointsCoords[numPoints][3] = {
215235
{ 0, 0, 0 },
216236
{ 0, 1, 0 },
217237
{ 0, 1, 1 },
218-
{ 0, 0, 1 } };
238+
{ 0, 0, 1 }
239+
};
240+
219241
vtkNew< vtkPoints > points;
220242
points->Allocate( numPoints );
221243
for( double const * pointsCoord: pointsCoords )
@@ -225,11 +247,12 @@ class TestFractureImport : public ::testing::Test
225247
fracture->SetPoints( points );
226248

227249
int constexpr numQuads = 1;
228-
vtkIdType const quad[numQuads][4] = { { 0, 1, 2, 3 } };
250+
int constexpr pointsPerQuad = 4;
251+
vtkIdType const quad[numQuads][pointsPerQuad] = { { 0, 1, 2, 3 } };
229252
fracture->Allocate( numQuads );
230253
for( vtkIdType const * q: quad )
231254
{
232-
fracture->InsertNextCell( VTK_QUAD, numPoints, q );
255+
fracture->InsertNextCell( VTK_QUAD, pointsPerQuad, q );
233256
}
234257

235258
vtkNew< vtkIdTypeArray > cellGlobalIds;
@@ -250,15 +273,15 @@ class TestFractureImport : public ::testing::Test
250273
}
251274
fracture->GetPointData()->SetGlobalIds( pointGlobalIds );
252275

253-
// Do not forget the collocated_nodes fields
276+
// Collocated nodes - connects hex 0 and hex 1
254277
vtkNew< vtkIdTypeArray > collocatedNodes;
255278
collocatedNodes->SetName( "collocated_nodes" );
256279
collocatedNodes->SetNumberOfComponents( 2 );
257280
collocatedNodes->SetNumberOfTuples( numPoints );
258-
collocatedNodes->SetTuple2( 0, 4, 8 );
259-
collocatedNodes->SetTuple2( 1, 5, 9 );
260-
collocatedNodes->SetTuple2( 2, 6, 10 );
261-
collocatedNodes->SetTuple2( 3, 7, 11 );
281+
collocatedNodes->SetTuple2( 0, 4, 8 ); // Main mesh points 4 and 8
282+
collocatedNodes->SetTuple2( 1, 5, 9 ); // Main mesh points 5 and 9
283+
collocatedNodes->SetTuple2( 2, 6, 10 ); // Main mesh points 6 and 10
284+
collocatedNodes->SetTuple2( 3, 7, 11 ); // Main mesh points 7 and 11
262285

263286
fracture->GetPointData()->AddArray( collocatedNodes );
264287
}
@@ -289,29 +312,36 @@ TEST_F( TestFractureImport, fracture )
289312
// Instead of checking each rank on its own,
290313
// we check that all the data is present across the ranks.
291314
auto const sum = []( auto i ) // Alias
315+
292316
{
293317
return MpiWrapper::sum( i );
294318
};
295319

296-
// Volumic mesh validations
297-
ASSERT_EQ( sum( cellBlockManager.numNodes() ), 16 );
298-
ASSERT_EQ( sum( cellBlockManager.numEdges() ), 24 );
299-
ASSERT_EQ( sum( cellBlockManager.numFaces() ), 12 );
320+
// Volumic mesh validations - 3 hexahedra with 24 points
321+
// Points are NOT merged even though some are at same coordinates
322+
// because they have different global IDs (4-7 vs 8-11)
323+
ASSERT_EQ( sum( cellBlockManager.numNodes() ), 24 );
324+
// Edges and faces will be different too - just check they exist
325+
ASSERT_GT( sum( cellBlockManager.numEdges() ), 0 );
326+
ASSERT_GT( sum( cellBlockManager.numFaces() ), 0 );
300327

301328
// Fracture mesh validations
302329
ASSERT_EQ( sum( cellBlockManager.getFaceBlocks().numSubGroups() ), MpiWrapper::commSize() );
303330
FaceBlockABC const & faceBlock = cellBlockManager.getFaceBlocks().getGroup< FaceBlockABC >( 0 );
304331
ASSERT_EQ( sum( faceBlock.num2dElements() ), 1 );
305332
ASSERT_EQ( sum( faceBlock.num2dFaces() ), 4 );
333+
306334
auto ecn = faceBlock.get2dElemsToCollocatedNodesBuckets();
307335
auto const num2dElems = ecn.size();
308336
ASSERT_EQ( sum( num2dElems ), 1 );
337+
309338
auto numNodesInFrac = 0;
310339
for( int ei = 0; ei < num2dElems; ++ei )
311340
{
312341
numNodesInFrac += ecn[ei].size();
313342
}
314343
ASSERT_EQ( sum( numNodesInFrac ), 4 );
344+
315345
for( int ei = 0; ei < num2dElems; ++ei )
316346
{
317347
for( int ni = 0; ni < numNodesInFrac; ++ni )
@@ -327,7 +357,6 @@ TEST_F( TestFractureImport, fracture )
327357
TestMeshImport( m_vtkFile, validate, "fracture" );
328358
}
329359

330-
331360
TEST( VTKImport, cube )
332361
{
333362
auto validate = []( CellBlockManagerABC const & cellBlockManager ) -> void

src/coreComponents/mesh/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ if( ENABLE_VTK )
215215
generators/VTKMeshGenerator.hpp
216216
generators/VTKMeshGeneratorTools.hpp
217217
generators/VTKWellGenerator.hpp
218+
generators/VTKSuperCellPartitioning.hpp
218219
generators/VTKUtilities.hpp
219220
)
220221
set( mesh_sources ${mesh_sources}
@@ -224,6 +225,7 @@ if( ENABLE_VTK )
224225
generators/VTKMeshGenerator.cpp
225226
generators/VTKMeshGeneratorTools.cpp
226227
generators/VTKWellGenerator.cpp
228+
generators/VTKSuperCellPartitioning.cpp
227229
generators/VTKUtilities.cpp
228230
)
229231
list( APPEND tplDependencyList VTK::IOLegacy VTK::FiltersParallelDIY2 )

src/coreComponents/mesh/generators/CollocatedNodes.cpp

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,29 +24,35 @@ namespace geos::vtk
2424
{
2525

2626
CollocatedNodes::CollocatedNodes( string const & faceBlockName,
27-
vtkSmartPointer< vtkDataSet > faceMesh )
27+
vtkSmartPointer< vtkDataSet > faceMesh,
28+
bool performGlobalCheck )
2829
{
2930
// The vtk field to the collocated nodes for fractures.
3031
string const COLLOCATED_NODES = "collocated_nodes";
3132

3233
vtkIdTypeArray const * collocatedNodes = vtkIdTypeArray::FastDownCast( faceMesh->GetPointData()->GetArray( COLLOCATED_NODES.c_str() ) );
3334

34-
// Depending on the parallel split, the vtk face mesh may be empty on a rank.
35-
// In that case, vtk will not provide any field for the emtpy mesh.
36-
// Therefore, not finding the duplicated nodes field on a rank cannot be interpreted as a globally missing field.
37-
// Converting the address into an integer and exchanging it through the MPI ranks let us find out
38-
// if the field is globally missing or not.
39-
std::uintptr_t const address = MpiWrapper::max( reinterpret_cast< std::uintptr_t >(collocatedNodes) );
40-
if( address == 0 )
35+
// With performGlobalCheck, a NULL on the local rank is OK if any other rank found the field
36+
// (vtk does not provide fields for empty meshes). Otherwise we require it on every rank.
37+
// After MPI max a non-zero result means the field exists on at
38+
// least one rank, so the field is not globally missing.
39+
bool const isMissing = performGlobalCheck
40+
? ( MpiWrapper::max( reinterpret_cast< std::uintptr_t >( collocatedNodes ) ) == 0 )
41+
: ( collocatedNodes == nullptr );
42+
43+
if( isMissing )
4144
{
42-
GEOS_LOG_RANK_0( "Available point data fields in '" << faceBlockName << "':" );
45+
GEOS_LOG_RANK_0( GEOS_FMT( "Available point data fields in '{}':", faceBlockName ) );
4346
for( int i = 0; i < faceMesh->GetPointData()->GetNumberOfArrays(); ++i )
4447
{
45-
GEOS_LOG_RANK_0( " - " << faceMesh->GetPointData()->GetArrayName( i ) << " of type '" << faceMesh->GetPointData()->GetArray( i )->GetDataTypeAsString() << "'" );
48+
GEOS_LOG_RANK_0( GEOS_FMT( " - {} of type '{}'",
49+
faceMesh->GetPointData()->GetArrayName( i ),
50+
faceMesh->GetPointData()->GetArray( i )->GetDataTypeAsString() ) );
4651
}
47-
GEOS_ERROR( GEOS_FMT( "Could not find valid field \"{}\" for fracture \"{}\".",
52+
GEOS_ERROR( GEOS_FMT( "Could not find valid field \"{}\" for fracture \"{}\"{}.",
4853
COLLOCATED_NODES,
49-
faceBlockName ) );
54+
faceBlockName,
55+
performGlobalCheck ? "" : " on this rank" ) );
5056
}
5157

5258
if( collocatedNodes )

src/coreComponents/mesh/generators/CollocatedNodes.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,12 @@ class CollocatedNodes
3535
* @brief Build a convenience wrapper around the raw vtk collocated nodes information.
3636
* @param faceBlockName The face block name.
3737
* @param faceMesh The face mesh for which the collocated nodes structure will be fed.
38+
* @param performGlobalCheck Whether to check across all MPI ranks if field is missing (default: true).
39+
* Set to false when only calling on a subset of ranks.
3840
*/
3941
CollocatedNodes( string const & faceBlockName,
40-
vtkSmartPointer< vtkDataSet > faceMesh );
42+
vtkSmartPointer< vtkDataSet > faceMesh,
43+
bool performGlobalCheck = true );
4144

4245
/**
4346
* @brief For node @p i of the face block, returns all the duplicated global node indices in the main 3d mesh.

src/coreComponents/mesh/generators/ParMETISInterface.cpp

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ partition( ArrayOfArraysView< idx_t const, idx_t > const & graph,
9797
MPI_Comm comm,
9898
int const numRefinements )
9999
{
100+
GEOS_ERROR_IF( numParts <= 0, "Number of partitions must be strictly positive" );
101+
100102
array1d< idx_t > part( graph.size() ); // all 0 by default
101103
if( numParts == 1 )
102104
{
@@ -147,5 +149,61 @@ partition( ArrayOfArraysView< idx_t const, idx_t > const & graph,
147149
return part;
148150
}
149151

152+
array1d< idx_t >
153+
partitionWeighted( ArrayOfArraysView< idx_t const, idx_t > const & graph,
154+
arrayView1d< idx_t const > const & vertexWeights,
155+
arrayView1d< idx_t const > const & vertDist,
156+
idx_t const numParts,
157+
MPI_Comm comm,
158+
int const numRefinements )
159+
{
160+
GEOS_ERROR_IF( numParts <= 0, "Number of partitions must be strictly positive" );
161+
162+
array1d< idx_t > part( graph.size() );
163+
if( numParts == 1 )
164+
{
165+
return part;
166+
}
167+
168+
array1d< real_t > tpwgts( numParts );
169+
tpwgts.setValues< serialPolicy >( 1.0f / static_cast< real_t >( numParts ) );
170+
171+
idx_t wgtflag = 2; // vertex weights only
172+
idx_t numflag = 0;
173+
idx_t ncon = 1;
174+
idx_t npart = numParts;
175+
176+
// Options: [use_defaults, log_level, seed, coupling]
177+
idx_t options[4] = { 1, 0, 2022, PARMETIS_PSR_UNCOUPLED };
178+
179+
idx_t edgecut = 0;
180+
real_t ubvec = 1.05;
181+
182+
GEOS_PARMETIS_CHECK( ParMETIS_V3_PartKway(
183+
const_cast< idx_t * >( vertDist.data() ),
184+
const_cast< idx_t * >( graph.getOffsets() ),
185+
const_cast< idx_t * >( graph.getValues() ),
186+
const_cast< idx_t * >( vertexWeights.data() ),
187+
nullptr, // edge weights
188+
&wgtflag,
189+
&numflag, &ncon, &npart, tpwgts.data(),
190+
&ubvec, options, &edgecut, part.data(), &comm ) );
191+
192+
for( int iter = 0; iter < numRefinements; ++iter )
193+
{
194+
GEOS_PARMETIS_CHECK( ParMETIS_V3_RefineKway(
195+
const_cast< idx_t * >( vertDist.data() ),
196+
const_cast< idx_t * >( graph.getOffsets() ),
197+
const_cast< idx_t * >( graph.getValues() ),
198+
const_cast< idx_t * >( vertexWeights.data() ),
199+
nullptr,
200+
&wgtflag,
201+
&numflag, &ncon, &npart, tpwgts.data(),
202+
&ubvec, options, &edgecut, part.data(), &comm ) );
203+
}
204+
205+
return part;
206+
}
207+
150208
} // namespace parmetis
151209
} // namespace geos

src/coreComponents/mesh/generators/ParMETISInterface.hpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,23 @@ partition( ArrayOfArraysView< pmet_idx_t const, pmet_idx_t > const & graph,
6565
MPI_Comm comm,
6666
int const numRefinements );
6767

68+
/**
69+
* @brief Partition a graph with vertex weights
70+
* @param graph The adjacency graph
71+
* @param vertexWeights The vertex weights for load balancing
72+
* @param vertDist The element distribution
73+
* @param numParts The number of partitions
74+
* @param comm The MPI communicator
75+
* @param numRefinements Number of refinement passes
76+
* @return Partition assignment for each vertex
77+
*/
78+
array1d< pmet_idx_t >
79+
partitionWeighted( ArrayOfArraysView< pmet_idx_t const, pmet_idx_t > const & graph,
80+
arrayView1d< pmet_idx_t const > const & vertexWeights,
81+
arrayView1d< pmet_idx_t const > const & vertDist,
82+
pmet_idx_t const numParts,
83+
MPI_Comm comm,
84+
int const numRefinements );
6885
} // namespace parmetis
6986
} // namespace geos
7087

0 commit comments

Comments
 (0)