Skip to content

fix: 2D/3D cell co-location in parallel mesh redistribution#3967

Merged
castelletto1 merged 17 commits intodevelopfrom
fix/bd713/partition2Delement
Mar 27, 2026
Merged

fix: 2D/3D cell co-location in parallel mesh redistribution#3967
castelletto1 merged 17 commits intodevelopfrom
fix/bd713/partition2Delement

Conversation

@bd713
Copy link
Copy Markdown
Contributor

@bd713 bd713 commented Feb 9, 2026

Problem

The current redistributeMeshes() partitions the entire mesh (both 2D and 3D cells) all at once using graph partitioning.
However, there is no guarantee that a 2D surface element will be assigned to a rank containing its adjacent 3D face.
This can lead to configurations where 2D cells are orphaned on ranks with no access to their 3D neighbor faces.
See screenshot below for a real life illustration.
A side effect is that the nodes of these orphaned 2D surfaces are seen as owned by that rank and will prevent them from being properly ghosted in the 3D mesh.

Solution

In this PR, the redistribution workflow is refactored in 3 steps to ensure co-location of 2D and 3D cells:

  1. Separate 2D and 3D cells
  2. Redistribute 3D cells independently using existing graph partitioning routines
  3. Assign each 2D cell to the rank owning one of its 3D neighbors (deterministic tie-breaking uses minimum global ID)

NB: When using MeshDoctor splitting, the fracture elements suffer the exact same problem. These will be addressed in a separate PR.

image

Rebaseline

As the 2D elements are no longer directly included in the graph to be partitioned, the resulting partitions may differ from the baseline.
This notably affect the following tests:

  • impermeableFault_smoke_04
  • permeableFault_smoke_04

In addition, we now reject 2D orphan cells that resulted from a bug in MeshDoctor (geosPythonPackages fix (PR #219)). This required changing the split mesh for:

  • ALM_singlephasePoromechanics_curvedFrac_smoke_01
  • ALM_singlephasePoromechanics_curvedFrac_smoke_04
  • ALM_multiphasePoromechanics_curvedFrac_smoke_01
  • ALM_multiphasePoromechanics_curvedFrac_smoke_04

DENEL Bertrand added 3 commits February 8, 2026 20:12
@bd713 bd713 self-assigned this Feb 9, 2026
@bd713 bd713 added type: bug Something isn't working ci: run integrated tests Allows to run the integrated tests in GEOS CI labels Feb 9, 2026
@bd713 bd713 changed the title Fix: 2D/3D cell co-location in parallel mesh redistribution fix: 2D/3D cell co-location in parallel mesh redistribution Feb 9, 2026
@rrsettgast
Copy link
Copy Markdown
Member

@bd713 why not just merge the 2d and 3d meshes into a single mesh object?

@castelletto1 castelletto1 added flag: requires rebaseline Requires rebaseline branch in integratedTests ci: run CUDA builds Allows to triggers (costly) CUDA jobs ci: run code coverage enables running of the code coverage CI jobs labels Mar 19, 2026
@castelletto1 castelletto1 marked this pull request as draft March 19, 2026 17:51
@castelletto1 castelletto1 marked this pull request as ready for review March 19, 2026 17:52
return result;
}

/**
Copy link
Copy Markdown
Collaborator

@castelletto1 castelletto1 Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
/**
* @brief Extract cells by index and create index mapping to original mesh
*
* @param[in] mesh Source mesh
* @param[in] indices Cell indices to extract
* @param[out] output Deep copy of extracted cells
* @param[out] mapping Mapping from output cell indices to original mesh indices
*/
static void extractCellsByIndices( vtkDataSet & mesh,
vtkIdList * indices,
vtkSmartPointer< vtkUnstructuredGrid > & output,
array1d< vtkIdType > & mapping )
{
vtkNew< vtkExtractCells > extractor;
extractor->SetInputDataObject( &mesh );
extractor->SetExtractAllCells( false );
extractor->SetCellList( indices );
extractor->Update();
output = vtkSmartPointer< vtkUnstructuredGrid >::New();
output->DeepCopy( extractor->GetOutput() );
mapping.resize( indices->GetNumberOfIds() );
for( vtkIdType i = 0; i < indices->GetNumberOfIds(); ++i )
{
mapping[i] = indices->GetId( i );
}
}
/**

Adding an helper function to avoid doing the same thing for 3D and 2D cells.

Is there a way to avoid using DeepCopy?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added helper function.
Switched to shallowCopy and also removed the deep copies that separated 2D and 3D cells. Now rely on indices.

Comment on lines +883 to +915
// Extract and deep-copy 3D cells
vtkNew< vtkExtractCells > extractor3D;
extractor3D->SetInputDataObject( &mesh );
extractor3D->SetExtractAllCells( false );
extractor3D->SetCellList( indices3D );
extractor3D->Update();

cells3D = vtkSmartPointer< vtkUnstructuredGrid >::New();
cells3D->DeepCopy( extractor3D->GetOutput() );

// Store index mapping for 3D cells
cells3DToOriginal.resize( indices3D->GetNumberOfIds() );
for( vtkIdType i = 0; i < indices3D->GetNumberOfIds(); ++i )
{
cells3DToOriginal[i] = indices3D->GetId( i );
}

// Extract and deep-copy 2D cells
vtkNew< vtkExtractCells > extractor2D;
extractor2D->SetInputDataObject( &mesh );
extractor2D->SetExtractAllCells( false );
extractor2D->SetCellList( indices2D );
extractor2D->Update();

cells2D = vtkSmartPointer< vtkUnstructuredGrid >::New();
cells2D->DeepCopy( extractor2D->GetOutput() );

// Store index mapping for 2D cells
cells2DToOriginal.resize( indices2D->GetNumberOfIds() );
for( vtkIdType i = 0; i < indices2D->GetNumberOfIds(); ++i )
{
cells2DToOriginal[i] = indices2D->GetId( i );
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Extract and deep-copy 3D cells
vtkNew< vtkExtractCells > extractor3D;
extractor3D->SetInputDataObject( &mesh );
extractor3D->SetExtractAllCells( false );
extractor3D->SetCellList( indices3D );
extractor3D->Update();
cells3D = vtkSmartPointer< vtkUnstructuredGrid >::New();
cells3D->DeepCopy( extractor3D->GetOutput() );
// Store index mapping for 3D cells
cells3DToOriginal.resize( indices3D->GetNumberOfIds() );
for( vtkIdType i = 0; i < indices3D->GetNumberOfIds(); ++i )
{
cells3DToOriginal[i] = indices3D->GetId( i );
}
// Extract and deep-copy 2D cells
vtkNew< vtkExtractCells > extractor2D;
extractor2D->SetInputDataObject( &mesh );
extractor2D->SetExtractAllCells( false );
extractor2D->SetCellList( indices2D );
extractor2D->Update();
cells2D = vtkSmartPointer< vtkUnstructuredGrid >::New();
cells2D->DeepCopy( extractor2D->GetOutput() );
// Store index mapping for 2D cells
cells2DToOriginal.resize( indices2D->GetNumberOfIds() );
for( vtkIdType i = 0; i < indices2D->GetNumberOfIds(); ++i )
{
cells2DToOriginal[i] = indices2D->GetId( i );
}
// Extract and deep-copy 3D cells
extractCellsByIndices( mesh, indices3D, cells3D, cells3DToOriginal );
// Extract and deep-copy 2D cells
extractCellsByIndices( mesh, indices2D, cells2D, cells2DToOriginal );

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

"Global cell IDs must be present in mesh for 2D-3D neighbor mapping" );


// Build reverse lookup: original mesh index -> global cell ID (3D cells only)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Build reverse lookup: original mesh index -> global cell ID (3D cells only)
// Build reverse lookup: original mesh index -> global cell ID (3D cells only)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines +979 to +985
// Only consider 3D cells
if( mesh.GetCell( neighborIdx )->GetCellDimension() == 3 )
{
auto it = original3DToGlobalId.find( neighborIdx );
GEOS_ERROR_IF( it == original3DToGlobalId.end(),
GEOS_FMT( "3D neighbor at index {} not found in 3D cell mapping", neighborIdx ) );

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Only consider 3D cells
if( mesh.GetCell( neighborIdx )->GetCellDimension() == 3 )
{
auto it = original3DToGlobalId.find( neighborIdx );
GEOS_ERROR_IF( it == original3DToGlobalId.end(),
GEOS_FMT( "3D neighbor at index {} not found in 3D cell mapping", neighborIdx ) );
// Check if neighbor is a 3D cell and get its global ID
auto it = original3DToGlobalId.find( neighborIdx );
if( it != original3DToGlobalId.end() ) // Is 3D cell
{
neighbor3DGlobalIds.emplace_back( it->second );
}
// Non-3D neighbors (2D/1D/0D) are silently skipped

Comment on lines +1106 to +1120
// Compute offsets
array1d< int > offsets( numRanks );
int totalCells = 0;
for( int r = 0; r < numRanks; ++r )
{
offsets[r] = totalCells;
totalCells += cellCounts[r];
}

// Gather local global IDs
array1d< int64_t > localGlobalIds( local3DCells );
for( vtkIdType i = 0; i < local3DCells; ++i )
{
localGlobalIds[i] = static_cast< int64_t >( redistributed3DGlobalIds->GetTuple1( i ) );
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since global IDs are int64_t, I would expect offsets to be as well int64_t and not int.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

comm );

// Build complete partition map: global cell ID -> owning rank
stdUnorderedMap< int64_t, int64_t > complete3DPartitionMap;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
stdUnorderedMap< int64_t, int64_t > complete3DPartitionMap;
stdUnorderedMap< int64_t, int > complete3DPartitionMap;

Rank is int

for( int i = 0; i < cellCounts[r]; ++i )
{
int64_t const globalId = allGlobalIds[offsets[r] + i];
complete3DPartitionMap.emplace( globalId, r ); // ← FIXED
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
complete3DPartitionMap.emplace( globalId, r ); // ← FIXED
complete3DPartitionMap.emplace( globalId, r );

Comment on lines +1122 to +1126
// All-gather global IDs to all ranks
array1d< int64_t > allGlobalIds( totalCells );
MpiWrapper::allgatherv( localGlobalIds.data(), static_cast< int >( local3DCells ),
allGlobalIds.data(), cellCounts.data(), offsets.data(),
comm );
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to be very expensive for large meshes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Limited gather operation to only the relevant indices

Comment on lines +1197 to +1200
//GEOS_LOG_RANK( GEOS_FMT( "Rank merged {} 3D cells + {} 2D cells = {} total",
// redistributed3D->GetNumberOfCells(),
// local2DCells->GetNumberOfCells(),
// mergedMesh->GetNumberOfCells() ) );
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//GEOS_LOG_RANK( GEOS_FMT( "Rank merged {} 3D cells + {} 2D cells = {} total",
// redistributed3D->GetNumberOfCells(),
// local2DCells->GetNumberOfCells(),
// mergedMesh->GetNumberOfCells() ) );

Remove

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

Comment on lines +1032 to +1033
assign2DCellsTo3DPartitions( ArrayOfArrays< vtkIdType, int64_t > const & neighbors2Dto3D,
stdUnorderedMap< int64_t, int64_t > const & globalIdTo3DPartition )
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assign2DCellsTo3DPartitions( ArrayOfArrays< vtkIdType, int64_t > const & neighbors2Dto3D,
stdUnorderedMap< int64_t, int64_t > const & globalIdTo3DPartition )
assign2DCellsTo3DPartitions( ArrayOfArrays< vtkIdType, int64_t > const & neighbors2Dto3D,
stdUnorderedMap< int64_t, int > const & globalIdTo3DPartition )

Comment on lines +1047 to +1054
int64_t minGlobalId = neighbors[0];
for( localIndex n = 1; n < neighbors.size(); ++n )
{
if( neighbors[n] < minGlobalId )
{
minGlobalId = neighbors[n];
}
}
Copy link
Copy Markdown
Collaborator

@castelletto1 castelletto1 Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
int64_t minGlobalId = neighbors[0];
for( localIndex n = 1; n < neighbors.size(); ++n )
{
if( neighbors[n] < minGlobalId )
{
minGlobalId = neighbors[n];
}
}
int64_t minGlobalId = *std::min_element( neighbors.begin(), neighbors.end() );

*
* @param[in] neighbors2Dto3D Neighbor map from build2DTo3DNeighbors()
* @param[in] globalIdTo3DPartition Complete map of 3D cell global ID -> rank assignment
* @param[in] comm MPI communicator (unused, kept for API consistency)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param[in] comm MPI communicator (unused, kept for API consistency)

Comment on lines +1043 to +1044
GEOS_ASSERT_MSG( neighbors.size() > 0,
"2D cell should have at least one neighbor (enforced by build2DTo3DNeighbors)" );
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ASSERT or ERROR? Is the check needed here? build2DTo3DNeighbors should catch this earlier.

@castelletto1 castelletto1 merged commit acd7cc5 into develop Mar 27, 2026
22 checks passed
@castelletto1 castelletto1 deleted the fix/bd713/partition2Delement branch March 27, 2026 21:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci: run code coverage enables running of the code coverage CI jobs ci: run CUDA builds Allows to triggers (costly) CUDA jobs ci: run integrated tests Allows to run the integrated tests in GEOS CI flag: requires rebaseline Requires rebaseline branch in integratedTests type: bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants