#14611 Nested hybrid grid: number sibling LGRs consistently

A refinement level with a single reconstructed LGR keeps the bare level name (LGR_NHG_L2). A level with several LGRs now numbers all of them with 1-based suffixes (LGR_NHG_L4_1 .. LGR_NHG_L4_N) instead of leaving the first one without a suffix.
This commit is contained in:
Kristian Bendiksen
2026-08-26 14:59:25 +02:00
parent d7ea4351e3
commit d760925090
2 changed files with 88 additions and 42 deletions
@@ -152,7 +152,8 @@ bool RigNestedHybridGridReconstructor::reconstruct( RigEclipseCaseData* caseData
parentsByFactor[factor].insert( parent );
}
int componentIndex = 0;
// Collect the connected components first so the component count is known before naming.
std::vector<std::pair<Factor, std::vector<CoarseIjk>>> components;
for ( const auto& [factor, parents] : parentsByFactor )
{
std::set<CoarseIjk> remaining = parents;
@@ -180,53 +181,60 @@ bool RigNestedHybridGridReconstructor::reconstruct( RigEclipseCaseData* caseData
}
}
CoarseIjk c0 = { INT_MAX, INT_MAX, INT_MAX };
CoarseIjk c1 = { 0, 0, 0 };
for ( const CoarseIjk& parent : component )
for ( int axis = 0; axis < 3; axis++ )
{
c0[axis] = std::min( c0[axis], parent[axis] );
c1[axis] = std::max( c1[axis], parent[axis] );
}
components.push_back( { factor, std::move( component ) } );
}
}
const cvf::Vec3st dims( (size_t)( c1[0] - c0[0] + 1 ) * factor[0],
(size_t)( c1[1] - c0[1] + 1 ) * factor[1],
(size_t)( c1[2] - c0[2] + 1 ) * factor[2] );
std::vector<size_t> boxToParent( dims.x() * dims.y() * dims.z(), cvf::UNDEFINED_SIZE_T );
for ( size_t lk = 0; lk < dims.z(); lk++ )
for ( size_t lj = 0; lj < dims.y(); lj++ )
for ( size_t li = 0; li < dims.x(); li++ )
{
size_t local = naturalIndex( li, lj, lk, dims.x(), dims.y() );
size_t oi = (size_t)c0[0] + li / factor[0];
size_t oj = (size_t)c0[1] + lj / factor[1];
size_t ok = (size_t)c0[2] + lk / factor[2];
boxToParent[local] = naturalIndex( oi - 1, oj - 1, ( ok - 1 ) * kFactor, nx, ny );
}
for ( size_t componentIndex = 0; componentIndex < components.size(); componentIndex++ )
{
const auto& [factor, component] = components[componentIndex];
std::vector<size_t> boxToFlat( dims.x() * dims.y() * dims.z(), cvf::UNDEFINED_SIZE_T );
for ( const CoarseIjk& parent : component )
CoarseIjk c0 = { INT_MAX, INT_MAX, INT_MAX };
CoarseIjk c1 = { 0, 0, 0 };
for ( const CoarseIjk& parent : component )
for ( int axis = 0; axis < 3; axis++ )
{
const auto& coordinates = coordinatesByParent[parent];
for ( size_t f : cellsByParent[parent] )
{
const std::array<size_t, 3> flatIjk = { f % nx, ( f / nx ) % ny, f / ( nx * ny ) };
size_t localIjk[3];
for ( int axis = 0; axis < 3; axis++ )
{
const auto coordinateIt = std::lower_bound( coordinates[axis].begin(), coordinates[axis].end(), flatIjk[axis] );
localIjk[axis] = (size_t)( parent[axis] - c0[axis] ) * factor[axis] +
(size_t)( coordinateIt - coordinates[axis].begin() );
}
boxToFlat[naturalIndex( localIjk[0], localIjk[1], localIjk[2], dims.x(), dims.y() )] = f;
}
c0[axis] = std::min( c0[axis], parent[axis] );
c1[axis] = std::max( c1[axis], parent[axis] );
}
QString gridName = QString( "LGR_NHG_L%1" ).arg( level );
if ( componentIndex > 0 ) gridName += QString( "_%1" ).arg( componentIndex );
componentIndex++;
buildLocalGrid( caseData, grid, nextGridId++, gridName, dims, boxToFlat, boxToParent, sourceCells );
const cvf::Vec3st dims( (size_t)( c1[0] - c0[0] + 1 ) * factor[0],
(size_t)( c1[1] - c0[1] + 1 ) * factor[1],
(size_t)( c1[2] - c0[2] + 1 ) * factor[2] );
std::vector<size_t> boxToParent( dims.x() * dims.y() * dims.z(), cvf::UNDEFINED_SIZE_T );
for ( size_t lk = 0; lk < dims.z(); lk++ )
for ( size_t lj = 0; lj < dims.y(); lj++ )
for ( size_t li = 0; li < dims.x(); li++ )
{
size_t local = naturalIndex( li, lj, lk, dims.x(), dims.y() );
size_t oi = (size_t)c0[0] + li / factor[0];
size_t oj = (size_t)c0[1] + lj / factor[1];
size_t ok = (size_t)c0[2] + lk / factor[2];
boxToParent[local] = naturalIndex( oi - 1, oj - 1, ( ok - 1 ) * kFactor, nx, ny );
}
std::vector<size_t> boxToFlat( dims.x() * dims.y() * dims.z(), cvf::UNDEFINED_SIZE_T );
for ( const CoarseIjk& parent : component )
{
const auto& coordinates = coordinatesByParent[parent];
for ( size_t f : cellsByParent[parent] )
{
const std::array<size_t, 3> flatIjk = { f % nx, ( f / nx ) % ny, f / ( nx * ny ) };
size_t localIjk[3];
for ( int axis = 0; axis < 3; axis++ )
{
const auto coordinateIt = std::lower_bound( coordinates[axis].begin(), coordinates[axis].end(), flatIjk[axis] );
localIjk[axis] = (size_t)( parent[axis] - c0[axis] ) * factor[axis] +
(size_t)( coordinateIt - coordinates[axis].begin() );
}
boxToFlat[naturalIndex( localIjk[0], localIjk[1], localIjk[2], dims.x(), dims.y() )] = f;
}
}
// Single component on a level keeps the bare level name; several get 1-based suffixes.
QString gridName = QString( "LGR_NHG_L%1" ).arg( level );
if ( components.size() > 1 ) gridName += QString( "_%1" ).arg( componentIndex + 1 );
buildLocalGrid( caseData, grid, nextGridId++, gridName, dims, boxToFlat, boxToParent, sourceCells );
}
}
@@ -335,6 +335,44 @@ TEST_F( RigNestedHybridGridReconstructorTest, ReconstructFromOldIjk )
EXPECT_GT( checkedL2, 0 );
}
//--------------------------------------------------------------------------------------------------
/// LGR naming (#14611): a level with a single LGR keeps the bare level name, while a level with
/// several LGRs numbers all of them 1..N with no bare-named sibling. All names are unique.
//--------------------------------------------------------------------------------------------------
TEST_F( RigNestedHybridGridReconstructorTest, LgrNamesAreConsistent )
{
RigMainGrid* mainGrid = grid();
std::set<QString> uniqueNames;
std::map<QString, QStringList> namesByLevel;
for ( size_t i = 1; i < mainGrid->gridCount(); i++ )
{
const QString name = QString::fromStdString( mainGrid->gridByIndex( i )->gridName() );
EXPECT_TRUE( uniqueNames.insert( name ).second ) << "Duplicate LGR name: " << name.toStdString();
namesByLevel[name.section( '_', 0, 2 )].push_back( name ); // "LGR_NHG_L4_2" -> "LGR_NHG_L4"
}
for ( const auto& [levelName, names] : namesByLevel )
{
if ( names.size() == 1 )
{
EXPECT_EQ( names.front(), levelName );
}
else
{
QStringList expected;
for ( int n = 1; n <= names.size(); n++ )
expected.push_back( QString( "%1_%2" ).arg( levelName ).arg( n ) );
EXPECT_EQ( names, expected );
}
}
// The Drogon model has one LGR on levels 2 and 3 and several on level 4.
EXPECT_EQ( namesByLevel["LGR_NHG_L2"].size(), 1 );
EXPECT_EQ( namesByLevel["LGR_NHG_L3"].size(), 1 );
EXPECT_GT( namesByLevel["LGR_NHG_L4"].size(), 1 );
}
//--------------------------------------------------------------------------------------------------
/// Results propagate to the reconstructed LGR cells: an active-cell-indexed STATIC result and the
/// full-length REFINE input property (both loaded before reconstruction) must both carry, on an LGR