From d760925090dc4b9cac47d2f504babf4d69453a68 Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Wed, 26 Aug 2026 12:06:38 +0200 Subject: [PATCH] #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. --- .../RigNestedHybridGridReconstructor.cpp | 92 ++++++++++--------- .../RigNestedHybridGridReconstructor-Test.cpp | 38 ++++++++ 2 files changed, 88 insertions(+), 42 deletions(-) diff --git a/ApplicationLibCode/ReservoirDataModel/RigNestedHybridGridReconstructor.cpp b/ApplicationLibCode/ReservoirDataModel/RigNestedHybridGridReconstructor.cpp index 07c08023aa..262c5668ce 100644 --- a/ApplicationLibCode/ReservoirDataModel/RigNestedHybridGridReconstructor.cpp +++ b/ApplicationLibCode/ReservoirDataModel/RigNestedHybridGridReconstructor.cpp @@ -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>> components; for ( const auto& [factor, parents] : parentsByFactor ) { std::set 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 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 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 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 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 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 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 ); } } diff --git a/ApplicationLibCode/UnitTests/RigNestedHybridGridReconstructor-Test.cpp b/ApplicationLibCode/UnitTests/RigNestedHybridGridReconstructor-Test.cpp index 36e2cf459a..376a9dc577 100644 --- a/ApplicationLibCode/UnitTests/RigNestedHybridGridReconstructor-Test.cpp +++ b/ApplicationLibCode/UnitTests/RigNestedHybridGridReconstructor-Test.cpp @@ -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 uniqueNames; + std::map 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