#14794 Cell filter: Use original parent IJK for IJK range filters on nested hybrid grids

For nested hybrid grids, IJK range filters (I/J/K slices and IJK filters)
now refer exclusively to the original (coarse) grid IJK. Nested-grid local
IJK values are no longer offered, and every cell, including cells in all
refinement levels, is mapped to the original IJK of its top-most parent.

- Add RigNestedHybridGridLayout describing how original cells are embedded
  in the flat grid, shared by the LGR reconstruction and the IJK mapping
- RigMainGrid: store the layout and compute the original IJK of any cell on
  demand by following the parent cell chain, without per-cell storage
- Range filter UI: slider limits, clamping, defaults and active cell range
  use the original grid dimensions; grid selection is hidden
- Evaluate range filters on all grids using the original IJK in the 3D view,
  combined filters and data filters
- 3D view context menu slices use the original IJK of the picked cell
- Add unit tests for the layout, the IJK mapping against OLDIJK, and slice
  filtering across refinement levels
This commit is contained in:
Kristian Bendiksen authored and Magne Sjaastad committed 2026-10-12 15:12:48 +02:00
1 parent 60f96f0ebd
commit 3d8912ba6c
15 files changed
+568 -79

No files matched your search

@@ -27,6 +27,7 @@
#include "RigCell.h"
#include "RigEclipseCaseData.h"
#include "RigGridBase.h"
#include "RigMainGrid.h"
#include "RigResultAccessorFactory.h"
#include "Rim3dOverlayInfoConfig.h"
@@ -751,9 +752,14 @@ void RivReservoirViewPartMgr::computeFilterVisibility( RivCellSetEnum
cvf::UByteArray indexIncludeVisibility = ( *cellVisibility );
cvf::UByteArray indexExcludeVisibility = ( *cellVisibility );
// Nested hybrid grid: IJK range filters are defined on the original grid IJK (main grid filters
// only), and every cell in every grid is evaluated using the original IJK it maps to.
const RigMainGrid* mainGrid = grid->mainGrid();
const bool useNestedHybridOrigIjk = mainGrid && mainGrid->nestedHybridLayout().has_value();
// Build cell filter for current grid
cvf::CellRangeFilter gridCellRangeFilter;
cellFilterColl->compoundCellRangeFilter( &gridCellRangeFilter, grid->gridIndex() );
cellFilterColl->compoundCellRangeFilter( &gridCellRangeFilter, useNestedHybridOrigIjk ? 0 : grid->gridIndex() );
cellFilterColl->updateCellVisibilityByIndex( &indexIncludeVisibility, &indexExcludeVisibility, grid->gridIndex() );
const RigLocalGrid* lgr = nullptr;
@@ -801,8 +807,33 @@ void RivReservoirViewPartMgr::computeFilterVisibility( RivCellSetEnum
size_t mainGridJ;
size_t mainGridK;
const bool isInSubGridArea = cell.subGrid() != nullptr;
grid->ijkFromCellIndex( cellIndex, &mainGridI, &mainGridJ, &mainGridK );
// Nested hybrid grid: LGR cells are evaluated directly against the main grid filter, and are
// part of a sub grid area (as their parent cell), so filters not propagated to sub grids skip them
const bool isInSubGridArea = cell.subGrid() != nullptr || ( useNestedHybridOrigIjk && lgr != nullptr );
bool hasRangeIjk = true;
if ( useNestedHybridOrigIjk )
{
auto originalIjk = mainGrid->nestedHybridOriginalIjk( grid, cellIndex );
hasRangeIjk = originalIjk.has_value();
if ( hasRangeIjk )
{
mainGridI = originalIjk->x();
mainGridJ = originalIjk->y();
mainGridK = originalIjk->z();
}
}
else
{
grid->ijkFromCellIndex( cellIndex, &mainGridI, &mainGridJ, &mainGridK );
}
// Cells without an original IJK (nested hybrid flat refined cells and padding) are never
// selected by an IJK range filter
const bool isRangeVisible = hasRangeIjk &&
gridCellRangeFilter.isCellVisible( mainGridI, mainGridJ, mainGridK, isInSubGridArea );
const bool isRangeExcluded = hasRangeIjk &&
gridCellRangeFilter.isCellExcluded( mainGridI, mainGridJ, mainGridK, isInSubGridArea );
bool nativeRangeVisibility = false;
@@ -812,18 +843,16 @@ void RivReservoirViewPartMgr::computeFilterVisibility( RivCellSetEnum
{
if ( useAndOperation )
{
nativeRangeVisibility = indexIncludeVisibility[cellIndex] &&
gridCellRangeFilter.isCellVisible( mainGridI, mainGridJ, mainGridK, isInSubGridArea );
nativeRangeVisibility = indexIncludeVisibility[cellIndex] && isRangeVisible;
}
else
{
nativeRangeVisibility = indexIncludeVisibility[cellIndex] ||
gridCellRangeFilter.isCellVisible( mainGridI, mainGridJ, mainGridK, isInSubGridArea );
nativeRangeVisibility = indexIncludeVisibility[cellIndex] || isRangeVisible;
}
}
else
{
nativeRangeVisibility = gridCellRangeFilter.isCellVisible( mainGridI, mainGridJ, mainGridK, isInSubGridArea );
nativeRangeVisibility = isRangeVisible;
}
}
else
@@ -831,8 +860,7 @@ void RivReservoirViewPartMgr::computeFilterVisibility( RivCellSetEnum
nativeRangeVisibility = indexIncludeVisibility[cellIndex];
}
( *cellVisibility )[cellIndex] = ( visibleDueToParentGrid || nativeRangeVisibility ) &&
!gridCellRangeFilter.isCellExcluded( mainGridI, mainGridJ, mainGridK, isInSubGridArea ) &&
( *cellVisibility )[cellIndex] = ( visibleDueToParentGrid || nativeRangeVisibility ) && !isRangeExcluded &&
indexExcludeVisibility[cellIndex];
}
}
@@ -19,6 +19,7 @@
#include "RimCellFilter.h"
#include "RigGridBase.h"
#include "RigMainGrid.h"
#include "RigReservoirGridTools.h"
#include "Rim3dView.h"
#include "RimCase.h"
@@ -220,9 +221,24 @@ void RimCellFilter::setGridIndex( int gridIndex )
//--------------------------------------------------------------------------------------------------
int RimCellFilter::gridIndex() const
{
// The original IJK of a nested hybrid grid is defined on the main grid only
if ( isRangeFilter() && usesNestedHybridOriginalIjk() ) return 0;
return m_gridIndex;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RimCellFilter::usesNestedHybridOriginalIjk() const
{
auto eclCase = dynamic_cast<RimEclipseCase*>( sourceCase() );
if ( !eclCase ) return false;
auto mainGrid = eclCase->mainGrid();
return mainGrid && mainGrid->nestedHybridLayout().has_value();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -267,7 +283,8 @@ void RimCellFilter::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering&
{
m_gridIndex.uiCapability()->setUiName( "Part" );
}
group->add( &m_gridIndex );
// Nested hybrid grids: range filters always use the original grid IJK, so there is no grid to choose
if ( !( isRangeFilter() && usesNestedHybridOriginalIjk() ) ) group->add( &m_gridIndex );
bool readOnlyState = isFilterControlled();
@@ -290,7 +307,7 @@ QString RimCellFilter::modeString() const
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
const cvf::StructGridInterface* RimCellFilter::selectedGrid() const
RimCase* RimCellFilter::sourceCase() const
{
// Case-level data filters have no Rim3dView ancestor; fall back to m_srcCase, which the
// owning RimDataFilterCollection / RimCombinedFilter propagates on add.
@@ -300,6 +317,16 @@ const cvf::StructGridInterface* RimCellFilter::selectedGrid() const
rimCase = rimView->ownerCase();
}
if ( !rimCase ) rimCase = m_srcCase();
return rimCase;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
const cvf::StructGridInterface* RimCellFilter::selectedGrid() const
{
RimCase* rimCase = sourceCase();
if ( !rimCase ) return nullptr;
int clampedIndex = gridIndex();
@@ -348,24 +375,33 @@ void RimCellFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility,
if ( isRangeFilter() )
{
// Nested hybrid grid: the filter is defined on the original grid IJK, and every cell in every
// grid is evaluated using the original IJK it maps to (LGR cells via their parent cells).
const RigMainGrid* mainGrid = grid->mainGrid();
const bool useNestedHybridOrigIjk = mainGrid && mainGrid->nestedHybridLayout().has_value();
// updateCompundFilter is a no-op when the range filter targets a different grid than the
// one being evaluated. Skip the cellVisibility mutation entirely so LGR cells are not
// erroneously blanked by a main-grid-targeted filter (legacy: parentGridVisibilities).
if ( gridIndex() != gIndx ) return;
const int filterGridIndex = useNestedHybridOrigIjk ? 0 : gIndx;
if ( gridIndex() != filterGridIndex ) return;
cvf::CellRangeFilter rf;
updateCompundFilter( &rf, gIndx );
updateCompundFilter( &rf, filterGridIndex );
const bool isSubGrid = !grid->isMainGrid();
for ( size_t cellIdx = 0; cellIdx < n; ++cellIdx )
{
size_t i = 0, j = 0, k = 0;
if ( grid->ijkFromCellIndex( cellIdx, &i, &j, &k ) )
std::optional<caf::VecIjk0> ijk = useNestedHybridOrigIjk ? mainGrid->nestedHybridOriginalIjk( grid, cellIdx )
: grid->ijkFromCellIndex( cellIdx );
if ( ijk )
{
// isCellVisible returns false when there are no include ranges (pure EXCLUDE
// mode), so also consult isCellExcluded to mark those cells as "in the set".
if ( rf.isCellVisible( i, j, k, isSubGrid ) || rf.isCellExcluded( i, j, k, isSubGrid ) )
if ( rf.isCellVisible( ijk->i(), ijk->j(), ijk->k(), isSubGrid ) ||
rf.isCellExcluded( ijk->i(), ijk->j(), ijk->k(), isSubGrid ) )
{
inSet[cellIdx] = 1;
}
@@ -87,6 +87,10 @@ public:
void setGridIndex( int gridIndex );
int gridIndex() const;
// Nested hybrid grid: IJK range filters refer exclusively to the original (coarse) grid IJK, and
// are evaluated on all grids by mapping every cell to its original IJK.
bool usesNestedHybridOriginalIjk() const;
void updateIconState();
void updateActiveState( bool isControlled );
@@ -116,6 +120,7 @@ protected:
RimEclipseCase* eclipseCase() const;
const cvf::StructGridInterface* selectedGrid() const;
RimCase* sourceCase() const;
QList<caf::PdmOptionItemInfo> calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override;
@@ -46,6 +46,9 @@ cvf::ref<cvf::UByteArray>
const bool isInclude = ( filter->filterMode() == RimCellFilter::INCLUDE );
// Nested hybrid grid: range filters use the original grid IJK and are evaluated on every grid
const bool usesNestedHybridOriginalIjk = caseData->mainGrid()->nestedHybridLayout().has_value();
// Grid local masks are kept to allow propagation of parent grid visibility into LGRs
std::vector<cvf::ref<cvf::UByteArray>> gridMasks( caseData->gridCount() );
@@ -61,7 +64,7 @@ cvf::ref<cvf::UByteArray>
{
// Range filters evaluate only on their target grid. On other grids an INCLUDE filter
// contributes no cells, while an EXCLUDE filter removes none.
const bool isTargetGrid = ( filter->gridIndex() == static_cast<int>( gridIndex ) );
const bool isTargetGrid = usesNestedHybridOriginalIjk || ( filter->gridIndex() == static_cast<int>( gridIndex ) );
if ( isTargetGrid )
{
filter->applyToCellVisibility( &gridMask, grid, timeStepIndex, eclipseCase );
@@ -22,10 +22,12 @@
#include "RiaApplication.h"
#include "RigActiveCellInfo.h"
#include "RigMainGrid.h"
#include "RigReservoirGridTools.h"
#include "Rim3dView.h"
#include "RimCase.h"
#include "RimEclipseCase.h"
#include "RimTools.h"
#include "cafPdmFieldScriptingCapability.h"
@@ -135,17 +137,15 @@ void RimCellRangeFilter::fieldChangedByUi( const caf::PdmFieldHandle* changedFie
if ( changedField == &m_gridIndex )
{
const cvf::StructGridInterface* grid = selectedGrid();
if ( grid && grid->cellCountI() > 0 && grid->cellCountJ() > 0 && grid->cellCountK() > 0 )
if ( auto counts = filterCellCounts() )
{
cellCountI = static_cast<int>( grid->cellCountI() );
cellCountI = static_cast<int>( counts->i() );
startIndexI = 1;
cellCountJ = static_cast<int>( grid->cellCountJ() );
cellCountJ = static_cast<int>( counts->j() );
startIndexJ = 1;
cellCountK = static_cast<int>( grid->cellCountK() );
cellCountK = static_cast<int>( counts->k() );
startIndexK = 1;
}
@@ -165,19 +165,84 @@ void RimCellRangeFilter::fieldChangedByUi( const caf::PdmFieldHandle* changedFie
//--------------------------------------------------------------------------------------------------
void RimCellRangeFilter::computeAndSetValidValues()
{
if ( auto counts = filterCellCounts() )
{
cellCountI = std::clamp( cellCountI.v(), 1, static_cast<int>( counts->i() ) );
startIndexI = std::clamp( startIndexI.v(), 1, static_cast<int>( counts->i() ) );
cellCountJ = std::clamp( cellCountJ.v(), 1, static_cast<int>( counts->j() ) );
startIndexJ = std::clamp( startIndexJ.v(), 1, static_cast<int>( counts->j() ) );
cellCountK = std::clamp( cellCountK.v(), 1, static_cast<int>( counts->k() ) );
startIndexK = std::clamp( startIndexK.v(), 1, static_cast<int>( counts->k() ) );
}
updateIconState();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::optional<cvf::Vec3st> RimCellRangeFilter::filterCellCounts() const
{
if ( usesNestedHybridOriginalIjk() )
{
auto eclCase = dynamic_cast<RimEclipseCase*>( sourceCase() );
if ( eclCase && eclCase->mainGrid() && eclCase->mainGrid()->nestedHybridLayout() )
{
return eclCase->mainGrid()->nestedHybridLayout()->originalCellCounts();
}
return std::nullopt;
}
const cvf::StructGridInterface* grid = selectedGrid();
if ( grid && grid->cellCountI() > 0 && grid->cellCountJ() > 0 && grid->cellCountK() > 0 )
{
cellCountI = std::clamp( cellCountI.v(), 1, static_cast<int>( grid->cellCountI() ) );
startIndexI = std::clamp( startIndexI.v(), 1, static_cast<int>( grid->cellCountI() ) );
cellCountJ = std::clamp( cellCountJ.v(), 1, static_cast<int>( grid->cellCountJ() ) );
startIndexJ = std::clamp( startIndexJ.v(), 1, static_cast<int>( grid->cellCountJ() ) );
cellCountK = std::clamp( cellCountK.v(), 1, static_cast<int>( grid->cellCountK() ) );
startIndexK = std::clamp( startIndexK.v(), 1, static_cast<int>( grid->cellCountK() ) );
return cvf::Vec3st( grid->cellCountI(), grid->cellCountJ(), grid->cellCountK() );
}
updateIconState();
return std::nullopt;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::optional<RigBoundingBoxIjk<caf::VecIjk0>> RimCellRangeFilter::activeCellIjkBoundingBox() const
{
auto rimView = firstAncestorOrThisOfType<Rim3dView>();
auto actCellInfo = RigReservoirGridTools::activeCellInfo( rimView );
if ( !actCellInfo ) return std::nullopt;
const cvf::StructGridInterface* mainGrid = RigReservoirGridTools::mainGrid( m_srcCase );
if ( !mainGrid || selectedGrid() != mainGrid ) return std::nullopt;
if ( !usesNestedHybridOriginalIjk() ) return actCellInfo->ijkBoundingBox();
// Nested hybrid grid: bounding box of the original IJK of all active cells (including LGR cells)
auto rigMainGrid = dynamic_cast<const RigMainGrid*>( mainGrid );
if ( !rigMainGrid ) return std::nullopt;
caf::VecIjk0 min = caf::VecIjk0::UNDEFINED;
caf::VecIjk0 max = caf::VecIjk0::ZERO;
bool hasCells = false;
for ( ReservoirCellIndex reservoirCellIndex : actCellInfo->activeReservoirCellIndices() )
{
size_t localCellIndex = 0;
auto grid = rigMainGrid->gridAndGridLocalIdxFromGlobalCellIdx( reservoirCellIndex.value(), &localCellIndex );
auto ijk = rigMainGrid->nestedHybridOriginalIjk( grid, localCellIndex );
if ( ijk )
{
for ( int axis = 0; axis < 3; axis++ )
{
min[axis] = std::min( min[axis], ( *ijk )[axis] );
max[axis] = std::max( max[axis], ( *ijk )[axis] );
}
hasCells = true;
}
}
if ( !hasCells ) return std::nullopt;
return RigBoundingBoxIjk<caf::VecIjk0>( min, max );
}
//--------------------------------------------------------------------------------------------------
@@ -224,22 +289,14 @@ std::map<QString, std::vector<caf::PdmFieldHandle*>> RimCellRangeFilter::quickAc
//--------------------------------------------------------------------------------------------------
void RimCellRangeFilter::setDefaultValues( int sliceDirection, int defaultSlice )
{
const cvf::StructGridInterface* grid = selectedGrid();
auto counts = filterCellCounts();
if ( !counts ) return;
if ( !grid ) return;
auto rimView = firstAncestorOrThisOfType<Rim3dView>();
auto actCellInfo = RigReservoirGridTools::activeCellInfo( rimView );
const cvf::StructGridInterface* mainGrid = RigReservoirGridTools::mainGrid( m_srcCase );
if ( grid == mainGrid && actCellInfo )
if ( auto bbox = activeCellIjkBoundingBox() )
{
const auto& bbox = actCellInfo->ijkBoundingBox();
// Adjust to Eclipse indexing
auto min = bbox.min().toOneBased();
auto max = bbox.max().toOneBased();
auto min = bbox->min().toOneBased();
auto max = bbox->max().toOneBased();
startIndexI = static_cast<int>( min.x() );
startIndexJ = static_cast<int>( min.y() );
@@ -253,9 +310,9 @@ void RimCellRangeFilter::setDefaultValues( int sliceDirection, int defaultSlice
startIndexI = 1;
startIndexJ = 1;
startIndexK = 1;
cellCountI = static_cast<int>( grid->cellCountI() );
cellCountJ = static_cast<int>( grid->cellCountJ() );
cellCountK = static_cast<int>( grid->cellCountK() );
cellCountI = static_cast<int>( counts->x() );
cellCountJ = static_cast<int>( counts->y() );
cellCountK = static_cast<int>( counts->z() );
}
switch ( sliceDirection )
@@ -288,24 +345,23 @@ void RimCellRangeFilter::defineEditorAttribute( const caf::PdmFieldHandle* field
return;
}
const cvf::StructGridInterface* grid = selectedGrid();
if ( !grid ) return;
auto counts = filterCellCounts();
if ( !counts ) return;
if ( field == &startIndexI || field == &cellCountI )
{
myAttr->m_minimum = 1;
myAttr->m_maximum = static_cast<int>( grid->cellCountI() );
myAttr->m_maximum = static_cast<int>( counts->x() );
}
else if ( field == &startIndexJ || field == &cellCountJ )
{
myAttr->m_minimum = 1;
myAttr->m_maximum = static_cast<int>( grid->cellCountJ() );
myAttr->m_maximum = static_cast<int>( counts->y() );
}
else if ( field == &startIndexK || field == &cellCountK )
{
myAttr->m_minimum = 1;
myAttr->m_maximum = static_cast<int>( grid->cellCountK() );
myAttr->m_maximum = static_cast<int>( counts->z() );
}
}
@@ -316,22 +372,13 @@ void RimCellRangeFilter::defineUiOrdering( QString uiConfigName, caf::PdmUiOrder
{
RimCellFilter::defineUiOrdering( uiConfigName, uiOrdering );
const cvf::StructGridInterface* grid = selectedGrid();
const cvf::StructGridInterface* mainGrid = RigReservoirGridTools::mainGrid( m_srcCase );
auto rimView = firstAncestorOrThisOfType<Rim3dView>();
auto actCellInfo = RigReservoirGridTools::activeCellInfo( rimView );
bool isRadialGrid = isMainGridRadial();
if ( grid == mainGrid && actCellInfo )
if ( auto bbox = activeCellIjkBoundingBox() )
{
const auto& bbox = actCellInfo->ijkBoundingBox();
// Adjust to Eclipse indexing
auto min = bbox.min().toOneBased();
auto max = bbox.max().toOneBased();
auto min = bbox->min().toOneBased();
auto max = bbox->max().toOneBased();
QString label;
@@ -409,7 +456,7 @@ void RimCellRangeFilter::updateCompundFilter( cvf::CellRangeFilter* cellRangeFil
{
CAF_ASSERT( cellRangeFilter );
if ( gridIndex != m_gridIndex ) return;
if ( gridIndex != this->gridIndex() ) return;
if ( filterMode() == RimCellFilter::INCLUDE )
{
@@ -23,7 +23,12 @@
#include "QuickAccess/RimFieldQuickAccessInterface.h"
#include "RimCellFilter.h"
#include "RigBoundingBoxIjk.h"
#include "cafPdmFieldCvfVec3d.h"
#include "cafVecIjk.h"
#include <optional>
class RigGridBase;
class RigMainGrid;
@@ -74,6 +79,13 @@ private:
void computeAndSetValidValues();
bool isMainGridRadial() const;
// Cell counts of the grid the filter IJK refers to: the original grid for nested hybrid grids,
// otherwise the selected grid. Returns nullopt if no valid grid is available.
std::optional<cvf::Vec3st> filterCellCounts() const;
// Active cell IJK bounding box (0-based) in the filter IJK space, available for the main grid only
std::optional<RigBoundingBoxIjk<caf::VecIjk0>> activeCellIjkBoundingBox() const;
caf::PdmField<QString> m_labelI;
caf::PdmField<QString> m_labelJ;
caf::PdmField<QString> m_labelK;
@@ -86,7 +86,7 @@ void RimUserDefinedFilter::updateCompundFilter( cvf::CellRangeFilter* cellRangeF
{
CAF_ASSERT( cellRangeFilter );
if ( gridIndex != m_gridIndex ) return;
if ( gridIndex != this->gridIndex() ) return;
if ( filterMode() == RimCellFilter::INCLUDE )
{
@@ -89,6 +89,7 @@ set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RigNonUniformRefinement.cpp
${CMAKE_CURRENT_LIST_DIR}/RigNestedHybridGridReconstructor.cpp
${CMAKE_CURRENT_LIST_DIR}/RigNestedHybridGridResultTools.cpp
${CMAKE_CURRENT_LIST_DIR}/RigNestedHybridGridLayout.cpp
)
list(APPEND CODE_SOURCE_FILES ${SOURCE_GROUP_SOURCE_FILES})
@@ -423,6 +423,49 @@ const std::map<size_t, size_t>& RigMainGrid::nestedHybridCoarseParents() const
return m_nestedHybridCoarseParents;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RigMainGrid::setNestedHybridLayout( const RigNestedHybridGridLayout& layout )
{
m_nestedHybridLayout = layout;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
const std::optional<RigNestedHybridGridLayout>& RigMainGrid::nestedHybridLayout() const
{
return m_nestedHybridLayout;
}
//--------------------------------------------------------------------------------------------------
/// Computed on demand: LGR cells walk their parent cell chain up to the main grid, and the flat main
/// grid IJK is converted using the layout. No per-cell storage is needed.
//--------------------------------------------------------------------------------------------------
std::optional<caf::VecIjk0> RigMainGrid::nestedHybridOriginalIjk( const RigGridBase* grid, size_t gridLocalCellIndex ) const
{
if ( !m_nestedHybridLayout || !grid ) return std::nullopt;
size_t cellIndex = gridLocalCellIndex;
while ( !grid->isMainGrid() )
{
auto lgr = dynamic_cast<const RigLocalGrid*>( grid );
if ( !lgr || cellIndex >= lgr->cellCount() ) return std::nullopt;
cellIndex = lgr->cell( cellIndex ).parentCellIndex();
grid = lgr->parentGrid();
if ( !grid || cellIndex == cvf::UNDEFINED_SIZE_T ) return std::nullopt;
}
if ( grid != this || cellIndex >= cellCount() ) return std::nullopt;
std::optional<caf::VecIjk0> flatIjk = ijkFromCellIndex( cellIndex );
if ( !flatIjk ) return std::nullopt;
return m_nestedHybridLayout->originalIjkFromFlatIjk( *flatIjk );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -23,6 +23,7 @@
#include "RigCell.h"
#include "RigGridBase.h"
#include "RigLocalGrid.h"
#include "RigNestedHybridGridLayout.h"
#include "cvfBoundingBox.h"
#include "cvfCollection.h"
@@ -31,6 +32,7 @@
#include <map>
#include <mutex>
#include <optional>
#include <vector>
class RigActiveCellInfo;
@@ -80,6 +82,16 @@ public:
void setNestedHybridCoarseParents( const std::map<size_t, size_t>& cellToCoarseParent );
const std::map<size_t, size_t>& nestedHybridCoarseParents() const;
// Nested hybrid grid: index layout of the original (coarse) grid embedded in the flat main grid.
// Not set for other grids.
void setNestedHybridLayout( const RigNestedHybridGridLayout& layout );
const std::optional<RigNestedHybridGridLayout>& nestedHybridLayout() const;
// Nested hybrid grid: the original (coarse) IJK of a cell in any grid of this main grid. LGR cells
// are followed through their parent cells (recursively) up to the main grid. Returns nullopt if
// this is not a nested hybrid grid, or the cell has no original IJK (refined flat cells, padding).
std::optional<caf::VecIjk0> nestedHybridOriginalIjk( const RigGridBase* grid, size_t gridLocalCellIndex ) const;
RigNNCData* nncData();
void setFaults( const cvf::Collection<RigFault>& faults );
@@ -164,8 +176,9 @@ private:
cvf::Collection<RigLocalGrid> m_localGrids; ///< List of all the LGR's in this reservoir
std::vector<size_t> m_gridIdToIndexMapping; ///< Mapping from LGR Id to index.
std::map<size_t, size_t> m_nestedHybridLgrSourceCells; ///< LGR cell global index -> source flat cell global index
std::map<size_t, size_t> m_nestedHybridCoarseParents; ///< flat cell global index -> parent coarse cell natural index
std::map<size_t, size_t> m_nestedHybridLgrSourceCells; ///< LGR cell global index -> source flat cell global index
std::map<size_t, size_t> m_nestedHybridCoarseParents; ///< flat cell global index -> parent coarse cell natural index
std::optional<RigNestedHybridGridLayout> m_nestedHybridLayout;
cvf::Collection<RigFault> m_faults;
cvf::ref<RigNNCData> m_nncData;
@@ -0,0 +1,66 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026- Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RigNestedHybridGridLayout.h"
#include <algorithm>
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RigNestedHybridGridLayout::RigNestedHybridGridLayout( const cvf::Vec3st& originalCellCounts, size_t kFactor )
: m_originalCellCounts( originalCellCounts )
, m_kFactor( std::max( kFactor, size_t( 1 ) ) )
{
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
const cvf::Vec3st& RigNestedHybridGridLayout::originalCellCounts() const
{
return m_originalCellCounts;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
size_t RigNestedHybridGridLayout::kFactor() const
{
return m_kFactor;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::optional<caf::VecIjk0> RigNestedHybridGridLayout::originalIjkFromFlatIjk( const caf::VecIjk0& flatIjk ) const
{
const size_t ok = flatIjk.z() / m_kFactor;
if ( flatIjk.x() >= m_originalCellCounts.x() || flatIjk.y() >= m_originalCellCounts.y() || ok >= m_originalCellCounts.z() )
return std::nullopt;
return caf::VecIjk0( flatIjk.x(), flatIjk.y(), ok );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
caf::VecIjk0 RigNestedHybridGridLayout::flatIjkFromOriginalIjk( const caf::VecIjk0& originalIjk ) const
{
return caf::VecIjk0( originalIjk.x(), originalIjk.y(), originalIjk.z() * m_kFactor );
}
@@ -0,0 +1,54 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026- Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "cafVecIjk.h"
#include "cvfVector3.h"
#include <optional>
//==================================================================================================
/// Index layout of a nested hybrid grid (see RigNestedHybridGridReconstructor).
///
/// The flat main grid embeds each original (coarse) cell (OI, OJ, OK) (0-based) at the flat position
/// (OI, OJ, OK * kFactor), where kFactor is the global K refinement factor. Flat columns outside the
/// original I/J extent hold the scattered refined cells (or padding) and have no original IJK.
///
/// This class is the single place defining that convention, used both when reconstructing the LGRs
/// and when mapping cells back to their original IJK (e.g. for IJK range filters).
//==================================================================================================
class RigNestedHybridGridLayout
{
public:
RigNestedHybridGridLayout( const cvf::Vec3st& originalCellCounts, size_t kFactor );
const cvf::Vec3st& originalCellCounts() const;
size_t kFactor() const;
// Original IJK of a flat main grid cell, or nullopt if the flat cell lies outside the original grid.
std::optional<caf::VecIjk0> originalIjkFromFlatIjk( const caf::VecIjk0& flatIjk ) const;
// Flat main grid IJK of the (host) cell representing an original cell.
caf::VecIjk0 flatIjkFromOriginalIjk( const caf::VecIjk0& originalIjk ) const;
private:
cvf::Vec3st m_originalCellCounts;
size_t m_kFactor;
};
@@ -29,6 +29,7 @@
#include "RigLocalGrid.h"
#include "RigMainGrid.h"
#include "RigNNCData.h"
#include "RigNestedHybridGridLayout.h"
#include "RigNestedHybridGridResultTools.h"
#include "RigNncConnection.h"
@@ -94,6 +95,8 @@ bool RigNestedHybridGridReconstructor::reconstruct( RigEclipseCaseData* caseData
}
const size_t kFactor = ( coarseNz > 0 && nz % (size_t)coarseNz == 0 ) ? nz / (size_t)coarseNz : 1;
const RigNestedHybridGridLayout layout( cvf::Vec3st( (size_t)coarseNx, (size_t)coarseNy, (size_t)coarseNz ), kFactor );
// Group all refined cells (REFINE > 1) by level, and record each refined cell's parent COARSE
// cell natural index (for the volume-weighted QC aggregate).
std::map<int, std::vector<size_t>> cellsByLevel;
@@ -206,11 +209,12 @@ bool RigNestedHybridGridReconstructor::reconstruct( RigEclipseCaseData* caseData
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 );
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];
caf::VecIjk0 host = layout.flatIjkFromOriginalIjk( caf::VecIjk0( oi - 1, oj - 1, ok - 1 ) );
boxToParent[local] = naturalIndex( host.x(), host.y(), host.z(), nx, ny );
}
std::vector<size_t> boxToFlat( dims.x() * dims.y() * dims.z(), cvf::UNDEFINED_SIZE_T );
@@ -242,6 +246,7 @@ bool RigNestedHybridGridReconstructor::reconstruct( RigEclipseCaseData* caseData
grid->setNestedHybridLgrSourceCells( sourceCells );
grid->setNestedHybridCoarseParents( coarseParents );
grid->setNestedHybridLayout( layout );
updateActiveCellInfo( caseData, sourceCells );
extendFullLengthResults( caseData, origCellCount, sourceCells );
@@ -38,6 +38,8 @@
#include "RigNestedHybridGridResultTools.h"
#include "RigNncConnection.h"
#include "RimCellFilterTools.h"
#include "RimCellRangeFilter.h"
#include "RimEclipseResultCase.h"
#include <QDir>
@@ -783,3 +785,158 @@ TEST_F( RigNestedHybridGridReconstructorTest, PerLevelSumAggregate )
}
EXPECT_GE( verified, 1 );
}
//--------------------------------------------------------------------------------------------------
/// The reconstruction records the original (coarse) grid layout: 15x24x12 coarse cells, with each
/// coarse K layer spanning 8 flat K layers (96 / 12).
//--------------------------------------------------------------------------------------------------
TEST_F( RigNestedHybridGridReconstructorTest, NestedHybridLayout )
{
const auto& layout = grid()->nestedHybridLayout();
ASSERT_TRUE( layout.has_value() );
EXPECT_EQ( layout->originalCellCounts(), cvf::Vec3st( 15, 24, 12 ) );
EXPECT_EQ( layout->kFactor(), 8u );
auto host = layout->flatIjkFromOriginalIjk( caf::VecIjk0( 3, 4, 5 ) );
EXPECT_EQ( host, caf::VecIjk0( 3, 4, 40 ) );
auto original = layout->originalIjkFromFlatIjk( caf::VecIjk0( 3, 4, 47 ) );
ASSERT_TRUE( original.has_value() );
EXPECT_EQ( *original, caf::VecIjk0( 3, 4, 5 ) );
// Flat columns outside the original I/J extent hold refined cells or padding
EXPECT_FALSE( layout->originalIjkFromFlatIjk( caf::VecIjk0( 15, 0, 0 ) ).has_value() );
EXPECT_FALSE( layout->originalIjkFromFlatIjk( caf::VecIjk0( 0, 24, 0 ) ).has_value() );
}
//--------------------------------------------------------------------------------------------------
/// The original IJK computed on demand (layout + parent cell chain) must agree with the OLDIJK
/// sidecar: for every reconstructed LGR cell, for every unrefined coarse cell, and the scattered
/// flat refined cells must have no original IJK.
//--------------------------------------------------------------------------------------------------
TEST_F( RigNestedHybridGridReconstructorTest, OriginalIjkFollowsOldIjk )
{
RigMainGrid* mainGrid = grid();
auto oldIjk0 = []( size_t flat )
{ return caf::VecIjk0( (size_t)( s_input.oldI[flat] - 1 ), (size_t)( s_input.oldJ[flat] - 1 ), (size_t)( s_input.oldK[flat] - 1 ) ); };
// LGR cells: original IJK equals OLDIJK of the flat cell they were built from
size_t lgrChecked = 0, lgrMismatch = 0;
for ( const auto& [lgrGlobal, flat] : mainGrid->nestedHybridLgrSourceCells() )
{
size_t localIdx = 0;
auto lgr = mainGrid->gridAndGridLocalIdxFromGlobalCellIdx( lgrGlobal, &localIdx );
ASSERT_FALSE( lgr->isMainGrid() );
auto original = mainGrid->nestedHybridOriginalIjk( lgr, localIdx );
if ( !original || *original != oldIjk0( flat ) ) lgrMismatch++;
lgrChecked++;
}
EXPECT_GT( lgrChecked, 0u );
EXPECT_EQ( lgrMismatch, 0u );
// Main grid cells
const size_t nx = mainGrid->cellCountI();
const size_t ny = mainGrid->cellCountJ();
size_t coarseChecked = 0, coarseMismatch = 0, refinedWithIjk = 0;
for ( size_t f = 0; f < mainGrid->cellCount(); f++ )
{
auto original = mainGrid->nestedHybridOriginalIjk( mainGrid, f );
if ( s_input.refine[f] > 1 )
{
// Scattered refined cells (now represented by LGR cells) have no original IJK
if ( original ) refinedWithIjk++;
continue;
}
if ( s_input.oldI[f] < 1 ) continue;
const caf::VecIjk0 flatIjk( f % nx, ( f / nx ) % ny, f / ( nx * ny ) );
const caf::VecIjk0 expected = oldIjk0( f );
if ( flatIjk != grid()->nestedHybridLayout()->flatIjkFromOriginalIjk( expected ) ) continue; // padding
if ( !original || *original != expected ) coarseMismatch++;
coarseChecked++;
}
EXPECT_GT( coarseChecked, 0u );
EXPECT_EQ( coarseMismatch, 0u );
EXPECT_EQ( refinedWithIjk, 0u );
}
//--------------------------------------------------------------------------------------------------
/// IJK range filters on a nested hybrid grid refer to the original grid IJK. A slice selects every
/// cell mapped to the original slice - unrefined cells and the cells of all refinement levels.
//--------------------------------------------------------------------------------------------------
TEST_F( RigNestedHybridGridReconstructorTest, RangeFilterUsesOriginalIjk )
{
RigMainGrid* mainGrid = grid();
s_resultCase->setReservoirData( s_reservoir.p() );
RimCellRangeFilter rangeFilter;
rangeFilter.setCase( s_resultCase.get() );
EXPECT_TRUE( rangeFilter.usesNestedHybridOriginalIjk() );
// A filter targeting an LGR is redirected to the original grid
rangeFilter.setGridIndex( 2 );
EXPECT_EQ( rangeFilter.gridIndex(), 0 );
// Original IJK level of every reconstructed LGR cell (via its source flat cell)
std::map<size_t, int> lgrLevel; // LGR global cell index -> refinement level
for ( const auto& [lgrGlobal, flat] : mainGrid->nestedHybridLgrSourceCells() )
lgrLevel[lgrGlobal] = s_input.refine[flat];
struct Slice
{
int axis;
int index; // 1-based original index
};
for ( const Slice& slice : { Slice{ 0, 10 }, Slice{ 1, 12 }, Slice{ 2, 5 } } )
{
rangeFilter.startIndexI = 1;
rangeFilter.startIndexJ = 1;
rangeFilter.startIndexK = 1;
rangeFilter.cellCountI = 15;
rangeFilter.cellCountJ = 24;
rangeFilter.cellCountK = 12;
if ( slice.axis == 0 ) std::tie( rangeFilter.startIndexI.v(), rangeFilter.cellCountI.v() ) = std::make_pair( slice.index, 1 );
if ( slice.axis == 1 ) std::tie( rangeFilter.startIndexJ.v(), rangeFilter.cellCountJ.v() ) = std::make_pair( slice.index, 1 );
if ( slice.axis == 2 ) std::tie( rangeFilter.startIndexK.v(), rangeFilter.cellCountK.v() ) = std::make_pair( slice.index, 1 );
auto visibility = RimCellFilterTools::computeReservoirCellVisibility( &rangeFilter, s_resultCase.get(), 0 );
ASSERT_TRUE( visibility.notNull() );
ASSERT_EQ( visibility->size(), mainGrid->totalCellCount() );
// Every LGR cell is visible exactly when its OLDIJK lies in the slice
size_t mismatch = 0;
std::set<int> visibleLevels;
const std::array oldIjk = { &s_input.oldI, &s_input.oldJ, &s_input.oldK };
for ( const auto& [lgrGlobal, flat] : mainGrid->nestedHybridLgrSourceCells() )
{
const bool expected = ( *oldIjk[slice.axis] )[flat] == slice.index;
const bool visible = visibility->val( lgrGlobal ) != 0;
if ( expected != visible ) mismatch++;
if ( visible ) visibleLevels.insert( lgrLevel[lgrGlobal] );
}
EXPECT_EQ( mismatch, 0u ) << "axis " << slice.axis;
// Main grid cells: only cells within the original columns, mapping to the slice, are visible
size_t mainVisible = 0, mainWrong = 0;
for ( size_t f = 0; f < mainGrid->cellCount(); f++ )
{
if ( !visibility->val( f ) ) continue;
mainVisible++;
auto original = mainGrid->nestedHybridOriginalIjk( mainGrid, f );
if ( !original || ( *original )[slice.axis] != (size_t)( slice.index - 1 ) ) mainWrong++;
}
EXPECT_GT( mainVisible, 0u ) << "axis " << slice.axis;
EXPECT_EQ( mainWrong, 0u ) << "axis " << slice.axis;
// The slices are chosen to cross more than one refinement level
EXPECT_GE( visibleLevels.size(), 2u ) << "axis " << slice.axis;
}
}
@@ -385,20 +385,39 @@ void RiuViewerCommands::displayContextMenu( QMouseEvent* event )
size_t i, j, k;
ijkFromCellIndex( mainOrComparisonView, m_currentGridIdx, m_currentCellIndex, &i, &j, &k );
size_t sliceGridIdx = m_currentGridIdx;
// Nested hybrid grid: slices always refer to the original grid IJK on the main grid
if ( auto eclipseView = dynamic_cast<RimEclipseView*>( mainOrComparisonView );
eclipseView && eclipseView->eclipseCase() && eclipseView->eclipseCase()->eclipseCaseData() )
{
const RigEclipseCaseData* caseData = eclipseView->eclipseCase()->eclipseCaseData();
if ( const RigMainGrid* mainGrid = caseData->mainGrid(); mainGrid && mainGrid->nestedHybridLayout() )
{
if ( auto originalIjk = mainGrid->nestedHybridOriginalIjk( caseData->grid( m_currentGridIdx ), m_currentCellIndex ) )
{
i = originalIjk->i();
j = originalIjk->j();
k = originalIjk->k();
}
sliceGridIdx = 0;
}
}
QVariantList iSliceList;
iSliceList.push_back( 0 );
iSliceList.push_back( std::max( static_cast<int>( i + 1 ), 1 ) );
iSliceList.push_back( static_cast<int>( m_currentGridIdx ) );
iSliceList.push_back( static_cast<int>( sliceGridIdx ) );
QVariantList jSliceList;
jSliceList.push_back( 1 );
jSliceList.push_back( std::max( static_cast<int>( j + 1 ), 1 ) );
jSliceList.push_back( static_cast<int>( m_currentGridIdx ) );
jSliceList.push_back( static_cast<int>( sliceGridIdx ) );
QVariantList kSliceList;
kSliceList.push_back( 2 );
kSliceList.push_back( std::max( static_cast<int>( k + 1 ), 1 ) );
kSliceList.push_back( static_cast<int>( m_currentGridIdx ) );
kSliceList.push_back( static_cast<int>( sliceGridIdx ) );
menuBuilder.subMenuStart( "Range Filter", QIcon( ":/CellFilter_Range.png" ) );