#14345 Add per-case cell filter evaluation with source case override

Add a sourceCaseOverride parameter to RimCellFilter::applyToCellVisibility.
When set, property filters evaluate against the override case's result
values instead of the case the filter is bound to, allowing the same
filter definition to be evaluated against each case in an ensemble.
Combined filters forward the override to their children, and geometry
based filters ignore it.

Add RimCellFilterTools::computeReservoirCellVisibility, which evaluates
a cell filter against a given case for all grids and returns the
visibility indexed by reservoir cell index. Range filters propagate the
parent grid visibility into LGR cells, matching the filtered geometry
of a 3d view.

Also add RimCellFilter::setFilterMode.
This commit is contained in:
Kristian Bendiksen committed 2026-09-16 10:53:31 +02:00
1 parent da28ffe613
commit e9b28c1b02
11 files changed
+377 -16

No files matched your search

@@ -1,6 +1,7 @@
set(SOURCE_GROUP_SOURCE_FILES set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RimCellFilter.cpp ${CMAKE_CURRENT_LIST_DIR}/RimCellFilter.cpp
${CMAKE_CURRENT_LIST_DIR}/RimCellFilterCollection.cpp ${CMAKE_CURRENT_LIST_DIR}/RimCellFilterCollection.cpp
${CMAKE_CURRENT_LIST_DIR}/RimCellFilterTools.cpp
${CMAKE_CURRENT_LIST_DIR}/RimCellRangeFilter.cpp ${CMAKE_CURRENT_LIST_DIR}/RimCellRangeFilter.cpp
${CMAKE_CURRENT_LIST_DIR}/RimCombinedFilter.cpp ${CMAKE_CURRENT_LIST_DIR}/RimCombinedFilter.cpp
${CMAKE_CURRENT_LIST_DIR}/RimDataFilterCollection.cpp ${CMAKE_CURRENT_LIST_DIR}/RimDataFilterCollection.cpp
@@ -199,6 +199,14 @@ caf::AppEnum<RimCellFilter::FilterModeType> RimCellFilter::filterMode() const
return m_filterMode(); return m_filterMode();
} }
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimCellFilter::setFilterMode( FilterModeType filterMode )
{
m_filterMode = filterMode;
}
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
/// ///
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
@@ -322,7 +330,10 @@ QList<caf::PdmOptionItemInfo> RimCellFilter::calculateValueOptions( const caf::P
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
/// ///
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
void RimCellFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility, const RigGridBase* grid, size_t /*timeStepIndex*/ ) void RimCellFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility,
const RigGridBase* grid,
size_t /*timeStepIndex*/,
RimEclipseCase* /*sourceCaseOverride*/ )
{ {
if ( cellVisibility == nullptr || grid == nullptr ) return; if ( cellVisibility == nullptr || grid == nullptr ) return;
@@ -79,6 +79,7 @@ public:
virtual bool isFilterEnabled() const; virtual bool isFilterEnabled() const;
caf::AppEnum<FilterModeType> filterMode() const; caf::AppEnum<FilterModeType> filterMode() const;
void setFilterMode( FilterModeType filterMode );
QString modeString() const; QString modeString() const;
bool propagateToSubGrids() const; bool propagateToSubGrids() const;
@@ -97,7 +98,13 @@ public:
// Unified evaluation: take an incoming per-cell visibility mask and hide the cells that // Unified evaluation: take an incoming per-cell visibility mask and hide the cells that
// this filter rejects (respecting its own INCLUDE/EXCLUDE mode). The default implementation // this filter rejects (respecting its own INCLUDE/EXCLUDE mode). The default implementation
// bridges the legacy range/index dispatch so existing subclasses do not need to override. // bridges the legacy range/index dispatch so existing subclasses do not need to override.
virtual void applyToCellVisibility( cvf::UByteArray* cellVisibility, const RigGridBase* grid, size_t timeStepIndex ); // When sourceCaseOverride is set, property filters evaluate against that case's results instead
// of the case the filter is bound to, allowing the same filter definition to be evaluated
// against each case in an ensemble.
virtual void applyToCellVisibility( cvf::UByteArray* cellVisibility,
const RigGridBase* grid,
size_t timeStepIndex,
RimEclipseCase* sourceCaseOverride = nullptr );
protected: protected:
caf::PdmFieldHandle* userDescriptionField() override; caf::PdmFieldHandle* userDescriptionField() override;
@@ -0,0 +1,103 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 "RimCellFilterTools.h"
#include "RigEclipseCaseData.h"
#include "RigGridBase.h"
#include "RigLocalGrid.h"
#include "RigMainGrid.h"
#include "RimCellFilter.h"
#include "RimEclipseCase.h"
//--------------------------------------------------------------------------------------------------
/// Evaluate the filter against the given case for all grids, and return the visibility indexed by
/// reservoir cell index. Property filters are evaluated against the given case's own result values.
//--------------------------------------------------------------------------------------------------
cvf::ref<cvf::UByteArray>
RimCellFilterTools::computeReservoirCellVisibility( RimCellFilter* filter, RimEclipseCase* eclipseCase, size_t timeStepIndex )
{
if ( !filter || !eclipseCase || !eclipseCase->eclipseCaseData() || !eclipseCase->eclipseCaseData()->mainGrid() ) return nullptr;
RigEclipseCaseData* caseData = eclipseCase->eclipseCaseData();
cvf::ref<cvf::UByteArray> reservoirVisibility = new cvf::UByteArray( caseData->mainGrid()->totalCellCount() );
reservoirVisibility->setAll( false );
const bool isInclude = ( filter->filterMode() == RimCellFilter::INCLUDE );
// Grid local masks are kept to allow propagation of parent grid visibility into LGRs
std::vector<cvf::ref<cvf::UByteArray>> gridMasks( caseData->gridCount() );
for ( size_t gridIndex = 0; gridIndex < caseData->gridCount(); gridIndex++ )
{
RigGridBase* grid = caseData->grid( gridIndex );
gridMasks[gridIndex] = new cvf::UByteArray( grid->cellCount() );
cvf::UByteArray& gridMask = *gridMasks[gridIndex];
gridMask.setAll( true );
if ( filter->isRangeFilter() )
{
// Range filters evaluate only on their target grid. On other grids an INCLUDE filter
// contributes no cells, while an EXCLUDE filter removes none. Cells in LGRs follow the
// visibility of their parent grid cell, as in the filtered geometry of a 3d view.
const bool isTargetGrid = ( filter->gridIndex() == static_cast<int>( gridIndex ) );
if ( isTargetGrid )
{
filter->applyToCellVisibility( &gridMask, grid, timeStepIndex, eclipseCase );
}
else
{
gridMask.setAll( !isInclude );
}
if ( !grid->isMainGrid() )
{
auto localGrid = static_cast<const RigLocalGrid*>( grid );
const cvf::UByteArray& parentMask = *gridMasks[localGrid->parentGrid()->gridIndex()];
for ( size_t localIdx = 0; localIdx < grid->cellCount(); localIdx++ )
{
const size_t parentCellIndex = grid->cell( localIdx ).parentCellIndex();
if ( isInclude )
{
gridMask[localIdx] = gridMask[localIdx] || parentMask[parentCellIndex];
}
else
{
gridMask[localIdx] = gridMask[localIdx] && parentMask[parentCellIndex];
}
}
}
}
else
{
// Index filters (polygon, user defined) and property filters evaluate on all grids
filter->applyToCellVisibility( &gridMask, grid, timeStepIndex, eclipseCase );
}
for ( size_t localIdx = 0; localIdx < grid->cellCount(); localIdx++ )
{
reservoirVisibility->set( grid->reservoirCellIndex( localIdx ), gridMask[localIdx] );
}
}
return reservoirVisibility;
}
@@ -0,0 +1,34 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 "cvfArray.h"
#include "cvfObject.h"
class RimCellFilter;
class RimEclipseCase;
//==================================================================================================
///
//==================================================================================================
class RimCellFilterTools
{
public:
static cvf::ref<cvf::UByteArray> computeReservoirCellVisibility( RimCellFilter* filter, RimEclipseCase* eclipseCase, size_t timeStepIndex );
};
@@ -104,7 +104,10 @@ void RimCombinedFilter::onGridChanged()
/// INCLUDE/EXCLUDE mode inside applyToCellVisibility), AND/OR combine the masks, then apply this /// INCLUDE/EXCLUDE mode inside applyToCellVisibility), AND/OR combine the masks, then apply this
/// combined filter's INCLUDE/EXCLUDE mode onto the incoming cellVisibility. /// combined filter's INCLUDE/EXCLUDE mode onto the incoming cellVisibility.
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
void RimCombinedFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility, const RigGridBase* grid, size_t timeStepIndex ) void RimCombinedFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility,
const RigGridBase* grid,
size_t timeStepIndex,
RimEclipseCase* sourceCaseOverride )
{ {
if ( cellVisibility == nullptr || grid == nullptr ) return; if ( cellVisibility == nullptr || grid == nullptr ) return;
@@ -125,7 +128,7 @@ void RimCombinedFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility,
{ {
cvf::UByteArray childMask( n ); cvf::UByteArray childMask( n );
childMask.setAll( 1 ); childMask.setAll( 1 );
child->applyToCellVisibility( &childMask, grid, timeStepIndex ); child->applyToCellVisibility( &childMask, grid, timeStepIndex, sourceCaseOverride );
if ( m_combineMode() == CombineMode::AND ) if ( m_combineMode() == CombineMode::AND )
{ {
@@ -49,7 +49,10 @@ public:
bool isFilterEnabled() const override; bool isFilterEnabled() const override;
void onGridChanged() override; void onGridChanged() override;
void applyToCellVisibility( cvf::UByteArray* cellVisibility, const RigGridBase* grid, size_t timeStepIndex ) override; void applyToCellVisibility( cvf::UByteArray* cellVisibility,
const RigGridBase* grid,
size_t timeStepIndex,
RimEclipseCase* sourceCaseOverride = nullptr ) override;
// Bridge from legacy collection dispatch: combined filter is declared INDEX-type, so the cell // Bridge from legacy collection dispatch: combined filter is declared INDEX-type, so the cell
// filter collection's evaluation path (if ever called) would route through updateCellIndexFilter. // filter collection's evaluation path (if ever called) would route through updateCellIndexFilter.
@@ -724,21 +724,36 @@ void RimEclipsePropertyFilter::initAfterRead()
/// uniformly; the body is extracted from the former inline loop in /// uniformly; the body is extracted from the former inline loop in
/// RivReservoirViewPartMgr::computePropertyVisibility. /// RivReservoirViewPartMgr::computePropertyVisibility.
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
void RimEclipsePropertyFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility, const RigGridBase* grid, size_t timeStepIndex ) void RimEclipsePropertyFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility,
const RigGridBase* grid,
size_t timeStepIndex,
RimEclipseCase* sourceCaseOverride )
{ {
if ( cellVisibility == nullptr || grid == nullptr ) return; if ( cellVisibility == nullptr || grid == nullptr ) return;
if ( !isActive() || !resultDefinition()->hasResult() ) return; if ( !isActive() || !resultDefinition()->hasResult() ) return;
resultDefinition()->loadResult(); RimEclipseCase* ec = sourceCaseOverride;
if ( ec )
// The result definition carries its own eclipse case binding (set via setEclipseCase). Prefer
// that, since case-level data filters have no view-side property-filter-collection ancestor.
RimEclipseCase* ec = resultDefinition()->eclipseCase();
if ( !ec )
{ {
auto* container = parentContainer(); // Evaluate against the override case's results. Make sure the result is loaded for that case.
auto* view = container ? container->reservoirView() : nullptr; if ( auto cellResultsData = ec->results( resultDefinition()->porosityModel() ) )
ec = view ? view->eclipseCase() : nullptr; {
cellResultsData->ensureKnownResultLoaded( resultDefinition()->eclipseResultAddress() );
}
}
else
{
resultDefinition()->loadResult();
// The result definition carries its own eclipse case binding (set via setEclipseCase). Prefer
// that, since case-level data filters have no view-side property-filter-collection ancestor.
ec = resultDefinition()->eclipseCase();
if ( !ec )
{
auto* container = parentContainer();
auto* view = container ? container->reservoirView() : nullptr;
ec = view ? view->eclipseCase() : nullptr;
}
} }
if ( !ec ) return; if ( !ec ) return;
RigEclipseCaseData* eclipseCase = ec->eclipseCaseData(); RigEclipseCaseData* eclipseCase = ec->eclipseCaseData();
@@ -59,7 +59,10 @@ public:
void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override; void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override;
void initAfterRead() override; void initAfterRead() override;
void applyToCellVisibility( cvf::UByteArray* cellVisibility, const RigGridBase* grid, size_t timeStepIndex ) override; void applyToCellVisibility( cvf::UByteArray* cellVisibility,
const RigGridBase* grid,
size_t timeStepIndex,
RimEclipseCase* sourceCaseOverride = nullptr ) override;
void updateUiFieldsFromActiveResult(); void updateUiFieldsFromActiveResult();
@@ -144,6 +144,7 @@ set(SOURCE_UNITTEST_FILES
${CMAKE_CURRENT_LIST_DIR}/opm-import-well-data-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/opm-import-well-data-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RifInpExportTools-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifInpExportTools-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RifGridCalculationIO-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifGridCalculationIO-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RimCellFilterTools-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RimGridCalculation-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RimGridCalculation-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RifSummaryCalculationIO-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifSummaryCalculationIO-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RimEmReader-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RimEmReader-Test.cpp
@@ -0,0 +1,180 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 "gtest/gtest.h"
#include "RiaDefines.h"
#include "RiaResultNames.h"
#include "RiaTestDataDirectory.h"
#include "RigActiveCellInfo.h"
#include "RigCaseCellResultsData.h"
#include "RigEclipseCaseData.h"
#include "RigEclipseResultAddress.h"
#include "RigMainGrid.h"
#include "RimCellFilterTools.h"
#include "RimCellRangeFilter.h"
#include "RimEclipsePropertyFilter.h"
#include "RimEclipseResultCase.h"
#include "RimEclipseResultDefinition.h"
#include "cafPdmField.h"
#include <QDir>
#include <QFile>
#include <cmath>
#include <memory>
static std::unique_ptr<RimEclipseResultCase> openBruggeCase( const QString& realizationFolder, const QString& fileName )
{
QDir baseFolder( TEST_MODEL_DIR );
if ( !baseFolder.cd( QString( "Case_with_10_timesteps/%1" ).arg( realizationFolder ) ) ) return nullptr;
QString filePath = baseFolder.absoluteFilePath( fileName );
if ( !QFile::exists( filePath ) ) return nullptr;
auto eclipseCase = std::make_unique<RimEclipseResultCase>();
eclipseCase->setCaseInfo( realizationFolder, filePath );
if ( !eclipseCase->openEclipseGridFile() ) return nullptr;
return eclipseCase;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RimCellFilterToolsTest, RangeFilterVisibility )
{
auto eclipseCase = openBruggeCase( "Real0", "BRUGGE_0000.EGRID" );
ASSERT_TRUE( eclipseCase != nullptr );
RimCellRangeFilter rangeFilter;
rangeFilter.setCase( eclipseCase.get() );
rangeFilter.startIndexI = 10;
rangeFilter.startIndexJ = 20;
rangeFilter.startIndexK = 1;
rangeFilter.cellCountI = 5;
rangeFilter.cellCountJ = 4;
rangeFilter.cellCountK = 3;
auto visibility = RimCellFilterTools::computeReservoirCellVisibility( &rangeFilter, eclipseCase.get(), 0 );
ASSERT_TRUE( visibility.notNull() );
const RigMainGrid* mainGrid = eclipseCase->eclipseCaseData()->mainGrid();
ASSERT_EQ( mainGrid->totalCellCount(), visibility->size() );
size_t visibleCount = 0;
for ( size_t i = 0; i < visibility->size(); i++ )
{
if ( visibility->val( i ) ) visibleCount++;
}
// The Brugge grid has no LGRs, so the geometric mask is exactly the IJK box
EXPECT_EQ( size_t( 5 * 4 * 3 ), visibleCount );
// An EXCLUDE filter must select the complement
rangeFilter.setFilterMode( RimCellFilter::EXCLUDE );
auto excludeVisibility = RimCellFilterTools::computeReservoirCellVisibility( &rangeFilter, eclipseCase.get(), 0 );
ASSERT_TRUE( excludeVisibility.notNull() );
size_t excludeVisibleCount = 0;
for ( size_t i = 0; i < excludeVisibility->size(); i++ )
{
if ( excludeVisibility->val( i ) ) excludeVisibleCount++;
}
EXPECT_EQ( visibility->size() - visibleCount, excludeVisibleCount );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RimCellFilterToolsTest, PropertyFilterVisibilityPerCase )
{
auto caseA = openBruggeCase( "Real0", "BRUGGE_0000.EGRID" );
auto caseB = openBruggeCase( "Real40", "BRUGGE_0040.EGRID" );
ASSERT_TRUE( caseA != nullptr );
ASSERT_TRUE( caseB != nullptr );
const double lowerBound = 0.3;
const double upperBound = 0.6;
// Property filter bound to case A
RimEclipsePropertyFilter propertyFilter;
propertyFilter.setCase( caseA.get() );
propertyFilter.resultDefinition()->setEclipseCase( caseA.get() );
propertyFilter.resultDefinition()->setResultType( RiaDefines::ResultCatType::DYNAMIC_NATIVE );
propertyFilter.resultDefinition()->setResultVariable( RiaResultNames::swat() );
auto* lowerField = dynamic_cast<caf::PdmField<double>*>( propertyFilter.findField( "LowerBound" ) );
auto* upperField = dynamic_cast<caf::PdmField<double>*>( propertyFilter.findField( "UpperBound" ) );
ASSERT_TRUE( lowerField && upperField );
lowerField->setValue( lowerBound );
upperField->setValue( upperBound );
const size_t timeStepIndex = 5;
auto countVisible = []( const cvf::UByteArray* visibility )
{
size_t count = 0;
for ( size_t i = 0; i < visibility->size(); i++ )
{
if ( visibility->val( i ) ) count++;
}
return count;
};
// Reference: count active cells with SWAT inside the bounds, using the case data directly
auto expectedVisibleCount = [&]( RimEclipseCase* eclipseCase )
{
auto porosityModel = RiaDefines::PorosityModelType::MATRIX_MODEL;
auto results = eclipseCase->results( porosityModel );
RigEclipseResultAddress swatAddress( RiaDefines::ResultCatType::DYNAMIC_NATIVE, RiaResultNames::swat() );
results->ensureKnownResultLoaded( swatAddress );
const auto& values = results->cellScalarResults( swatAddress, timeStepIndex );
size_t count = 0;
for ( double value : values )
{
if ( lowerBound <= value && value <= upperBound ) count++;
}
return count;
};
auto visibilityA = RimCellFilterTools::computeReservoirCellVisibility( &propertyFilter, caseA.get(), timeStepIndex );
auto visibilityB = RimCellFilterTools::computeReservoirCellVisibility( &propertyFilter, caseB.get(), timeStepIndex );
ASSERT_TRUE( visibilityA.notNull() );
ASSERT_TRUE( visibilityB.notNull() );
// The same filter definition must be evaluated against each case's own result values
EXPECT_EQ( expectedVisibleCount( caseA.get() ), countVisible( visibilityA.p() ) );
EXPECT_EQ( expectedVisibleCount( caseB.get() ), countVisible( visibilityB.p() ) );
// The realizations have different SWAT fields, so the masks must differ
ASSERT_EQ( visibilityA->size(), visibilityB->size() );
size_t differingCells = 0;
for ( size_t i = 0; i < visibilityA->size(); i++ )
{
if ( visibilityA->val( i ) != visibilityB->val( i ) ) differingCells++;
}
EXPECT_GT( differingCells, 0u );
}