#14710 Support ensemble data filters in ensemble contour maps

Replace the built-in formation filter with ensemble data filters. Load ensemble data filters after project read, and parent new views before assigning the case so the ensemble filter collection is resolved correctly.
This commit is contained in:
Magne Sjaastad committed 2026-09-28 10:30:12 +02:00
1 parent 2bdc35ef68
commit 9911c49d2b
20 files changed
+442 -190

No files matched your search

@@ -148,6 +148,7 @@ set(SOURCE_UNITTEST_FILES
${CMAKE_CURRENT_LIST_DIR}/RifInpExportTools-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RifGridCalculationIO-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RimCellFilterTools-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RimStatisticsContourMap-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RimGridCalculation-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RifSummaryCalculationIO-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RimEmReader-Test.cpp
@@ -28,6 +28,7 @@
#include "RigEclipseResultAddress.h"
#include "RigMainGrid.h"
#include "Formations/RimFormationNames.h"
#include "RimCellFilterTools.h"
#include "RimCellRangeFilter.h"
#include "RimDataFilterCollection.h"
@@ -47,9 +48,11 @@
#include <QDir>
#include <QFile>
#include <QTemporaryDir>
#include <cmath>
#include <memory>
#include <set>
static std::unique_ptr<RimEclipseResultCase> openBruggeCase( const QString& realizationFolder, const QString& fileName )
{
@@ -328,3 +331,71 @@ TEST( RimCellFilterToolsTest, PropertyFilterVisibilityPerCase )
}
EXPECT_GT( differingCells, 0u );
}
//--------------------------------------------------------------------------------------------------
/// Realizations can have their own formation names file, with the same formations covering different
/// K layers. A formation filter must use the K ranges of each realization.
//--------------------------------------------------------------------------------------------------
TEST( RimCellFilterToolsTest, FormationFilterUsesKLayersOfEachCase )
{
auto caseA = openBruggeCase( "Real0", "BRUGGE_0000.EGRID" );
auto caseB = openBruggeCase( "Real40", "BRUGGE_0040.EGRID" );
ASSERT_TRUE( caseA != nullptr );
ASSERT_TRUE( caseB != nullptr );
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
auto createFormationNames = [&tempDir]( const QString& fileName, const QString& content )
{
QFile file( tempDir.filePath( fileName ) );
EXPECT_TRUE( file.open( QIODevice::WriteOnly | QIODevice::Text ) );
file.write( content.toUtf8() );
file.close();
auto formationNames = std::make_unique<RimFormationNames>();
formationNames->setFileName( file.fileName() );
EXPECT_TRUE( formationNames->readFormationNamesFile().has_value() );
return formationNames;
};
auto formationsA = createFormationNames( "a.lyr", "'Upper' 1 - 2\n'Lower' 3 - 4\n" );
auto formationsB = createFormationNames( "b.lyr", "'Upper' 1 - 3\n'Lower' 4 - 4\n" );
caseA->eclipseCaseData()->setActiveFormationNames( formationsA->formationNamesData() );
caseB->eclipseCaseData()->setActiveFormationNames( formationsB->formationNamesData() );
RimEclipsePropertyFilter propertyFilter;
propertyFilter.setCase( caseA.get() );
propertyFilter.setActive( true );
propertyFilter.resultDefinition()->setResultType( RiaDefines::ResultCatType::FORMATION_NAMES );
propertyFilter.resultDefinition()->setResultVariable( RiaResultNames::activeFormationNamesResultName() );
propertyFilter.computeResultValueRange();
auto* categorySelectionField = dynamic_cast<caf::PdmField<bool>*>( propertyFilter.findField( "CategorySelection" ) );
auto* selectedValuesField = dynamic_cast<caf::PdmField<std::vector<int>>*>( propertyFilter.findField( "SelectedValues" ) );
ASSERT_TRUE( categorySelectionField && selectedValuesField );
categorySelectionField->setValue( true );
selectedValuesField->setValue( { 0 } );
ASSERT_TRUE( propertyFilter.isCategorySelectionActive() );
auto visibleKLayers = []( const cvf::UByteArray* visibility, const RimEclipseCase* eclipseCase )
{
const RigMainGrid* mainGrid = eclipseCase->eclipseCaseData()->mainGrid();
std::set<size_t> kLayers;
for ( size_t i = 0; i < visibility->size(); i++ )
{
size_t ci = 0, cj = 0, ck = 0;
if ( visibility->val( i ) && mainGrid->ijkFromCellIndex( i, &ci, &cj, &ck ) ) kLayers.insert( ck );
}
return kLayers;
};
auto visibilityA = RimCellFilterTools::computeReservoirCellVisibility( &propertyFilter, caseA.get(), 0 );
auto visibilityB = RimCellFilterTools::computeReservoirCellVisibility( &propertyFilter, caseB.get(), 0 );
ASSERT_TRUE( visibilityA.notNull() );
ASSERT_TRUE( visibilityB.notNull() );
EXPECT_EQ( std::set<size_t>( { 0, 1 } ), visibleKLayers( visibilityA.p(), caseA.get() ) );
EXPECT_EQ( std::set<size_t>( { 0, 1, 2 } ), visibleKLayers( visibilityB.p(), caseB.get() ) );
}
@@ -23,9 +23,11 @@
#include "RimCellRangeFilter.h"
#include "RimCombinedFilter.h"
#include "RimDataFilterCollection.h"
#include "RimDataFilterInView.h"
#include "RimDataFilterInViewCollection.h"
#include "RimEclipseResultCase.h"
#include "RimEclipseView.h"
#include "RimEclipseViewCollection.h"
#include "RimReservoirGridEnsemble.h"
#include "cafSignal.h"
@@ -246,3 +248,58 @@ TEST( RimDataFilterCollection, ensembleViewUsesEnsembleDataFilters )
ensemble->addView( view );
EXPECT_EQ( ensemble->dataFilterCollection(), view->dataFiltersInView()->sourceCollection() );
}
//--------------------------------------------------------------------------------------------------
/// A view created via a case's own view collection should still resolve the ensemble's data filters.
//--------------------------------------------------------------------------------------------------
TEST( RimDataFilterCollection, viewForSingleRealizationUsesEnsembleDataFilters )
{
auto ensemble = std::make_unique<RimReservoirGridEnsemble>();
auto* eclipseCase = new RimEclipseResultCase();
ensemble->addCase( eclipseCase );
RimEclipseView* view = eclipseCase->viewCollection()->addView( eclipseCase );
EXPECT_EQ( ensemble->dataFilterCollection(), view->dataFiltersInView()->sourceCollection() );
}
//--------------------------------------------------------------------------------------------------
/// A filter added to the ensemble after the view exists must still appear as a wrapper in the view.
//--------------------------------------------------------------------------------------------------
TEST( RimDataFilterCollection, viewWrapperAppearsWhenEnsembleFilterAddedAfterViewExists )
{
auto ensemble = std::make_unique<RimReservoirGridEnsemble>();
auto* eclipseCase = new RimEclipseResultCase();
ensemble->addCase( eclipseCase );
RimEclipseView* view = eclipseCase->viewCollection()->addView( eclipseCase );
ASSERT_EQ( ensemble->dataFilterCollection(), view->dataFiltersInView()->sourceCollection() );
EXPECT_TRUE( view->dataFiltersInView()->wrappers().empty() );
ensemble->dataFilterCollection()->addNewPropertyFilter();
ASSERT_EQ( size_t{ 1 }, view->dataFiltersInView()->wrappers().size() );
EXPECT_EQ( ensemble->dataFilterCollection()->filters().front(), view->dataFiltersInView()->wrappers().front()->sourceFilter() );
}
//--------------------------------------------------------------------------------------------------
/// Reproduces RicNewViewForGridEnsembleFeature's "New View" command: the view is created via the
/// ensemble's own view collection, which used to lose the ensemble's data filter because the case
/// was assigned before the view was parented. See #14710.
//--------------------------------------------------------------------------------------------------
TEST( RimDataFilterCollection, viewCreatedViaEnsembleViewCollectionUsesEnsembleDataFilters )
{
auto ensemble = std::make_unique<RimReservoirGridEnsemble>();
auto* eclipseCase = new RimEclipseResultCase();
ensemble->addCase( eclipseCase );
ensemble->dataFilterCollection()->addNewPropertyFilter();
// Exercises the same call path as RicNewViewForGridEnsembleFeature.
RimEclipseView* view = ensemble->viewCollection()->addView( eclipseCase );
EXPECT_EQ( ensemble->dataFilterCollection(), view->dataFiltersInView()->sourceCollection() );
ASSERT_EQ( size_t{ 1 }, view->dataFiltersInView()->wrappers().size() );
}
@@ -0,0 +1,225 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 "RiaTestDataDirectory.h"
#include "ContourMap/RigContourMapCalculator.h"
#include "RigEclipseCaseData.h"
#include "RigMainGrid.h"
#include "ContourMap/RimStatisticsContourMap.h"
#include "RimCellRangeFilter.h"
#include "RimDataFilterCollection.h"
#include "RimEclipseResultCase.h"
#include "RimEclipseResultDefinition.h"
#include "RimReservoirGridEnsemble.h"
#include "cafPdmChildField.h"
#include "cafPdmField.h"
#include "cafPdmPtrField.h"
#include <QDir>
#include <QFile>
#include <cmath>
#include <memory>
namespace
{
using GridModeType = RimReservoirGridEnsembleBase::GridModeType;
using GridImportMode = RimStatisticsContourMap::GridImportMode;
RimEclipseResultCase* openBruggeRealization( 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.release();
}
std::unique_ptr<RimReservoirGridEnsemble> createBruggeEnsemble( GridModeType gridMode )
{
auto ensemble = std::make_unique<RimReservoirGridEnsemble>();
for ( const auto& [folder, fileName] : std::vector<std::pair<QString, QString>>{ { "Real0", "BRUGGE_0000.EGRID" },
{ "Real10", "BRUGGE_0010.EGRID" },
{ "Real30", "BRUGGE_0030.EGRID" },
{ "Real40", "BRUGGE_0040.EGRID" } } )
{
auto* eclipseCase = openBruggeRealization( folder, fileName );
if ( !eclipseCase ) return nullptr;
ensemble->addCase( eclipseCase );
}
auto* autoDetectField = dynamic_cast<caf::PdmField<bool>*>( ensemble->findField( "AutoDetectGridType" ) );
auto* gridModeField = dynamic_cast<caf::PdmField<caf::AppEnum<GridModeType>>*>( ensemble->findField( "GridMode" ) );
if ( !autoDetectField || !gridModeField ) return nullptr;
autoDetectField->setValue( false );
gridModeField->setValue( gridMode );
return ensemble;
}
RimStatisticsContourMap* addPoroContourMap( RimReservoirGridEnsemble* ensemble, GridImportMode importMode )
{
auto* map = new RimStatisticsContourMap;
ensemble->addStatisticsContourMap( map );
map->setEclipseCase( ensemble->cases().front() );
map->setGridImportMode( importMode );
auto* aggregationField =
dynamic_cast<caf::PdmField<caf::AppEnum<RigContourMapCalculator::ResultAggregationType>>*>( map->findField( "ResultAggregation" ) );
auto* resultDefinitionField = dynamic_cast<caf::PdmChildField<RimEclipseResultDefinition*>*>( map->findField( "ResultDefinition" ) );
if ( !aggregationField || !resultDefinitionField ) return nullptr;
aggregationField->setValue( RigContourMapCalculator::MEAN );
( *resultDefinitionField )()->setResultType( RiaDefines::ResultCatType::STATIC_NATIVE );
( *resultDefinitionField )()->setResultVariable( "PORO" );
return map;
}
void setDataFilter( RimStatisticsContourMap* map, RimCellFilter* filter )
{
auto* dataFilterField = dynamic_cast<caf::PdmPtrField<RimCellFilter*>*>( map->findField( "DataFilter" ) );
ASSERT_TRUE( dataFilterField != nullptr );
dataFilterField->setValue( filter );
}
// Filter including the top K layer of the grid only
RimCellRangeFilter* addTopLayerFilter( RimReservoirGridEnsemble* ensemble )
{
const RigMainGrid* mainGrid = ensemble->cases().front()->eclipseCaseData()->mainGrid();
auto* rangeFilter = ensemble->dataFilterCollection()->addNewRangeFilter();
rangeFilter->startIndexI = 1;
rangeFilter->startIndexJ = 1;
rangeFilter->startIndexK = 1;
rangeFilter->cellCountI = static_cast<int>( mainGrid->cellCountI() );
rangeFilter->cellCountJ = static_cast<int>( mainGrid->cellCountJ() );
rangeFilter->cellCountK = 1;
return rangeFilter;
}
size_t countValidValues( const std::vector<double>& values )
{
return std::count_if( values.begin(), values.end(), []( double v ) { return std::isfinite( v ); } );
}
size_t countDifferentValues( const std::vector<double>& lhs, const std::vector<double>& rhs )
{
size_t differentCount = 0;
for ( size_t i = 0; i < lhs.size(); i++ )
{
const bool lhsValid = std::isfinite( lhs[i] );
const bool rhsValid = std::isfinite( rhs[i] );
if ( lhsValid != rhsValid || ( lhsValid && std::abs( lhs[i] - rhs[i] ) > 1.0e-9 ) ) differentCount++;
}
return differentCount;
}
//--------------------------------------------------------------------------------------------------
/// Compute the mean PORO contour map with and without a top layer data filter, and verify that the
/// filter changes the result
//--------------------------------------------------------------------------------------------------
std::vector<double> verifyDataFilterIsApplied( GridModeType gridMode, GridImportMode importMode )
{
auto ensemble = createBruggeEnsemble( gridMode );
if ( !ensemble ) ADD_FAILURE() << "Failed to create Brugge ensemble";
if ( !ensemble ) return {};
auto* map = addPoroContourMap( ensemble.get(), importMode );
if ( !map ) ADD_FAILURE() << "Failed to create contour map";
if ( !map ) return {};
RimStatisticsContourMap::computeStatisticsForMaps( { map } );
const auto unfiltered = map->result( 0, RimStatisticsContourMap::StatisticsType::MEAN );
EXPECT_GT( countValidValues( unfiltered ), 0u );
setDataFilter( map, addTopLayerFilter( ensemble.get() ) );
RimStatisticsContourMap::computeStatisticsForMaps( { map } );
const auto filtered = map->result( 0, RimStatisticsContourMap::StatisticsType::MEAN );
EXPECT_EQ( unfiltered.size(), filtered.size() );
if ( unfiltered.size() != filtered.size() ) return {};
EXPECT_GT( countValidValues( filtered ), 0u );
EXPECT_GT( countDifferentValues( unfiltered, filtered ), 0u );
// An EXCLUDE filter of the same layer must give yet another result
auto* excludeFilter = addTopLayerFilter( ensemble.get() );
excludeFilter->setFilterMode( RimCellFilter::EXCLUDE );
setDataFilter( map, excludeFilter );
RimStatisticsContourMap::computeStatisticsForMaps( { map } );
const auto excluded = map->result( 0, RimStatisticsContourMap::StatisticsType::MEAN );
EXPECT_GT( countDifferentValues( filtered, excluded ), 0u );
return filtered;
}
} // namespace
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RimStatisticsContourMapTest, DataFilterSharedGrid )
{
verifyDataFilterIsApplied( GridModeType::SHARED_GRID, GridImportMode::SHARED_GRID );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RimStatisticsContourMapTest, DataFilterIndividualGridImport )
{
verifyDataFilterIsApplied( GridModeType::SHARED_GRID, GridImportMode::INDIVIDUAL_GRIDS );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RimStatisticsContourMapTest, DataFilterIndividualGridEnsemble )
{
verifyDataFilterIsApplied( GridModeType::INDIVIDUAL_GRIDS, GridImportMode::SHARED_GRID );
}
//--------------------------------------------------------------------------------------------------
/// The Brugge realizations have identical grids, so the shared and individual grid code paths must
/// give the same filtered result
//--------------------------------------------------------------------------------------------------
TEST( RimStatisticsContourMapTest, DataFilterSharedAndIndividualGridsAgree )
{
const auto shared = verifyDataFilterIsApplied( GridModeType::SHARED_GRID, GridImportMode::SHARED_GRID );
const auto individual = verifyDataFilterIsApplied( GridModeType::INDIVIDUAL_GRIDS, GridImportMode::INDIVIDUAL_GRIDS );
ASSERT_FALSE( shared.empty() );
ASSERT_EQ( shared.size(), individual.size() );
EXPECT_EQ( 0u, countDifferentValues( shared, individual ) );
}