From 9911c49d2bbd62e0cce84da270c8600881da8f45 Mon Sep 17 00:00:00 2001 From: Magne Sjaastad Date: Sat, 26 Sep 2026 10:44:17 +0200 Subject: [PATCH] #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. --- .../RimDataFilterInViewCollection.cpp | 20 +- .../ContourMap/RimContourMapProjection.cpp | 10 +- .../ContourMap/RimContourMapProjection.h | 5 - .../ContourMap/RimStatisticsContourMap.cpp | 166 +++++-------- .../ContourMap/RimStatisticsContourMap.h | 7 +- .../ProjectDataModel/RimEclipseView.cpp | 3 + .../RimEclipseViewCollection.cpp | 6 +- .../RimReservoirGridEnsemble.cpp | 3 + .../ContourMap/RigContourMapCalculator.cpp | 23 +- .../ContourMap/RigContourMapCalculator.h | 7 +- .../ContourMap/RigContourMapProjection.cpp | 3 +- .../ContourMap/RigContourMapProjection.h | 1 - .../RigStatisticsContourMapProjection.cpp | 1 - .../RigStatisticsContourMapProjection.h | 1 - .../ReservoirDataModel/RigFormationNames.cpp | 19 -- .../ReservoirDataModel/RigFormationNames.h | 3 - ApplicationLibCode/UnitTests/CMakeLists.txt | 1 + .../UnitTests/RimCellFilterTools-Test.cpp | 71 ++++++ .../RimDataFilterCollection-Test.cpp | 57 +++++ .../RimStatisticsContourMap-Test.cpp | 225 ++++++++++++++++++ 20 files changed, 442 insertions(+), 190 deletions(-) create mode 100644 ApplicationLibCode/UnitTests/RimStatisticsContourMap-Test.cpp diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterInViewCollection.cpp b/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterInViewCollection.cpp index 5849d86d31..fb45235ead 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterInViewCollection.cpp +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterInViewCollection.cpp @@ -22,9 +22,6 @@ #include "RimCellFilter.h" #include "RimDataFilterCollection.h" #include "RimDataFilterInView.h" -#include "RimEclipseCase.h" -#include "RimEclipseView.h" -#include "RimReservoirGridEnsemble.h" #include "cafPdmUiTreeOrdering.h" @@ -161,21 +158,8 @@ bool RimDataFilterInViewCollection::hasActiveFilters() const //-------------------------------------------------------------------------------------------------- void RimDataFilterInViewCollection::initAfterRead() { - // setSourceCollection() is normally invoked from RimEclipseView::propagateEclipseCaseToChildObjects, - // which only runs when the case is assigned via the setter, not on plain XML load. Resolve the - // data filter collection through the PDM ancestry so wrappers always sync with the correct owner. - if ( auto* view = firstAncestorOrThisOfType() ) - { - if ( auto* gridEnsemble = view->firstAncestorOfType() ) - { - m_sourceCollection = gridEnsemble->dataFilterCollection(); - } - else if ( !m_sourceCollection() ) - { - if ( auto* eclCase = view->eclipseCase() ) m_sourceCollection = eclCase->dataFilterCollection(); - } - } - + // The source collection is re-resolved by RimEclipseView::initAfterRead() after this call, + // so just reconnect signals here. connectSourceSignal(); syncWithSource(); } diff --git a/ApplicationLibCode/ProjectDataModel/ContourMap/RimContourMapProjection.cpp b/ApplicationLibCode/ProjectDataModel/ContourMap/RimContourMapProjection.cpp index 0311f86c17..76caa633d9 100644 --- a/ApplicationLibCode/ProjectDataModel/ContourMap/RimContourMapProjection.cpp +++ b/ApplicationLibCode/ProjectDataModel/ContourMap/RimContourMapProjection.cpp @@ -147,7 +147,7 @@ void RimContourMapProjection::generateResultsIfNecessary( int timeStep ) if ( gridMappingNeedsUpdating() ) { m_contourMapProjection->setCellVisibility( getCellVisibility() ); - m_contourMapProjection->generateGridMapping( m_resultAggregation(), retrieveParameterWeights(), m_selectedKLayers, {} ); + m_contourMapProjection->generateGridMapping( m_resultAggregation(), retrieveParameterWeights(), {} ); } progress.setProgress( 20 ); m_mapCellVisibility = m_contourMapProjection->getMapCellVisibility( timeStep, m_resultAggregation() ); @@ -695,11 +695,3 @@ void RimContourMapProjection::clearMinMaxValueRange() m_minResultAllTimeSteps = std::numeric_limits::infinity(); m_maxResultAllTimeSteps = -std::numeric_limits::infinity(); } - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -void RimContourMapProjection::useKLayers( std::set kLayers ) -{ - m_selectedKLayers = kLayers; -} diff --git a/ApplicationLibCode/ProjectDataModel/ContourMap/RimContourMapProjection.h b/ApplicationLibCode/ProjectDataModel/ContourMap/RimContourMapProjection.h index 1a899c55f1..787d7d2b85 100644 --- a/ApplicationLibCode/ProjectDataModel/ContourMap/RimContourMapProjection.h +++ b/ApplicationLibCode/ProjectDataModel/ContourMap/RimContourMapProjection.h @@ -69,9 +69,6 @@ public: bool showContourLines() const; bool showContourLabels() const; - // k layer filter, only consider kLayers in the given set (0-based index) - void useKLayers( std::set kLayers ); - virtual QString resultAggregationText() const; QString caseName() const; @@ -172,8 +169,6 @@ protected: caf::PdmField m_upperThreshold; caf::PdmField m_lowerThreshold; - std::set m_selectedKLayers; - std::unique_ptr m_contourMapGrid; std::unique_ptr m_contourMapProjection; diff --git a/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.cpp b/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.cpp index 9f30fc442f..53fac64df1 100644 --- a/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.cpp +++ b/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.cpp @@ -34,16 +34,17 @@ #include "RigCaseCellResultsData.h" #include "RigEclipseCaseData.h" #include "RigEclipseResultAddress.h" -#include "RigFormationNames.h" #include "RigMainGrid.h" #include "RigPolyLinesData.h" #include "RigStatisticsMath.h" #include "ContourMap/RimContourMapInViewCollection.h" -#include "Formations/RimFormationNames.h" #include "Polygons/RimPolygon.h" #include "Polygons/RimPolygonCollection.h" #include "Rim3dView.h" +#include "RimCellFilter.h" +#include "RimCellFilterTools.h" +#include "RimDataFilterCollection.h" #include "RimEclipseCase.h" #include "RimEclipseCaseEnsemble.h" #include "RimEclipseContourMapProjection.h" @@ -74,24 +75,20 @@ #include #include #include -#include #include namespace { -std::optional> - findKLayersForFormations( RimEclipseCase* eCase, const std::vector& selectedFormations, RimFormationNames* fallbackFormationNames ) +//-------------------------------------------------------------------------------------------------- +/// Restrict the grid mapping to the cells accepted by the data filter, evaluated against the given +/// realization. The filter is evaluated at the first time step, as the grid mapping is shared by all +/// time steps. +//-------------------------------------------------------------------------------------------------- +void applyDataFilterVisibility( RigEclipseContourMapProjection& projection, RimCellFilter* dataFilter, RimEclipseCase* eCase ) { - if ( selectedFormations.empty() ) return std::set{}; + if ( !dataFilter ) return; - auto formationNames = eCase->activeFormationNames(); - if ( !formationNames ) formationNames = fallbackFormationNames; - if ( !formationNames ) return std::nullopt; - - auto fData = formationNames->formationNamesData(); - if ( !fData ) return std::nullopt; - - return fData->findKLayers( selectedFormations ); + projection.setCellVisibility( RimCellFilterTools::computeReservoirCellVisibility( dataFilter, eCase, 0 ) ); } void extractCaseResults( RigEclipseContourMapProjection& projection, @@ -197,15 +194,12 @@ RimStatisticsContourMap::RimStatisticsContourMap() CAF_PDM_InitFieldNoDefault( &m_views, "ContourMapViews", "Contour Maps", ":/CrossSection16x16.png" ); - CAF_PDM_InitField( &m_enableFormationFilter, "EnableFormationFilter", false, "Enable Formation Filter" ); - CAF_PDM_InitFieldNoDefault( &m_selectedFormations, "Formations", "Select Formations" ); - m_selectedFormations.uiCapability()->setUiEditorTypeName( caf::PdmUiTreeSelectionEditor::uiEditorTypeName() ); - m_selectedFormations.uiCapability()->setUiLabelPosition( caf::PdmUiItemInfo::LabelPosition::TOP ); - CAF_PDM_InitFieldNoDefault( &m_selectedPolygons, "Polygons", "Select Polygons" ); m_selectedPolygons.uiCapability()->setUiEditorTypeName( caf::PdmUiTreeSelectionEditor::uiEditorTypeName() ); m_selectedPolygons.uiCapability()->setUiLabelPosition( caf::PdmUiItemInfo::LabelPosition::TOP ); + CAF_PDM_InitFieldNoDefault( &m_dataFilter, "DataFilter", "Data Filter", "", "Only cells accepted by the ensemble data filter are used." ); + CAF_PDM_InitField( &m_cacheFileBaseName, "CacheFileBaseName", QString(), "Cache File Base Name" ); m_cacheFileBaseName.uiCapability()->setUiHidden( true ); @@ -241,14 +235,13 @@ void RimStatisticsContourMap::defineUiOrdering( QString uiConfigName, caf::PdmUi setEclipseCase( selCase ); } - bool computeOK = !( m_enableFormationFilter && m_selectedFormations().empty() ); - computeOK = computeOK && !selectedTimeSteps().empty(); + bool computeOK = !selectedTimeSteps().empty(); uiOrdering.add( nameField() ); { auto* btn = uiOrdering.addNewButton( "Compute", [this]() { onComputeStatisticsClicked(); } ); - btn->setUiToolTip( computeOK ? "Start statistics computations." : "Please check your time step and/or formation filter selections." ); + btn->setUiToolTip( computeOK ? "Start statistics computations." : "Please check your time step selection." ); btn->setUiReadOnly( !computeOK ); } @@ -290,12 +283,14 @@ void RimStatisticsContourMap::defineUiOrdering( QString uiConfigName, caf::PdmUi tsGroup->setCollapsedByDefault(); tsGroup->add( &m_selectedTimeSteps ); - if ( activeFormationNames() ) + if ( auto* gridEnsemble = firstAncestorOrThisOfType() ) { - auto formationGrp = uiOrdering.addNewGroup( "Formation Selection" ); - if ( !m_enableFormationFilter ) formationGrp->setCollapsedByDefault(); - formationGrp->add( &m_enableFormationFilter ); - if ( m_enableFormationFilter ) formationGrp->add( &m_selectedFormations ); + if ( m_dataFilter() || ( gridEnsemble->dataFilterCollection() && !gridEnsemble->dataFilterCollection()->filters().empty() ) ) + { + auto dataFilterGrp = uiOrdering.addNewGroup( "Data Filter" ); + if ( !m_dataFilter() ) dataFilterGrp->setCollapsedByDefault(); + dataFilterGrp->add( &m_dataFilter ); + } } if ( auto polygonCollection = RimTools::polygonCollection() ) @@ -361,22 +356,6 @@ QString RimStatisticsContourMap::ensembleName() const return {}; } -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -RimFormationNames* RimStatisticsContourMap::activeFormationNames() const -{ - if ( auto* gridCase = eclipseCase() ) - { - if ( auto* formationNames = gridCase->activeFormationNames() ) return formationNames; - } - if ( auto* ensemble = firstAncestorOrThisOfType() ) - { - return ensemble->activeFormationNames(); - } - return nullptr; -} - //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -465,19 +444,6 @@ QList RimStatisticsContourMap::calculateValueOptions( co } return options; } - else if ( &m_selectedFormations == fieldNeedingOptions ) - { - if ( auto formations = activeFormationNames() ) - { - if ( formations->formationNamesData() ) - { - for ( auto& f : formations->formationNamesData()->formationNames() ) - { - options.push_back( caf::PdmOptionItemInfo( f, f, false ) ); - } - } - } - } else if ( &m_selectedPolygons == fieldNeedingOptions ) { if ( auto polygonCollection = RimTools::polygonCollection() ) @@ -488,6 +454,22 @@ QList RimStatisticsContourMap::calculateValueOptions( co } } } + else if ( &m_dataFilter == fieldNeedingOptions ) + { + options.push_back( caf::PdmOptionItemInfo( "None", nullptr ) ); + + if ( auto* gridEnsemble = firstAncestorOrThisOfType() ) + { + if ( auto* dataFilterCollection = gridEnsemble->dataFilterCollection() ) + { + for ( RimCellFilter* filter : dataFilterCollection->filters() ) + { + if ( !filter ) continue; + options.push_back( caf::PdmOptionItemInfo( filter->fullName(), filter, false, filter->uiIconProvider() ) ); + } + } + } + } return options; } @@ -649,7 +631,6 @@ void RimStatisticsContourMap::computeStatisticsForMaps( const std::vector sharedProjection; - std::set kLayers; bool active = false; }; @@ -703,22 +684,13 @@ void RimStatisticsContourMap::computeStatisticsForMaps( const std::vectorselectedFormations(), map->activeFormationNames() ) ) - { - ctx.kLayers = *kLayers; + auto primaryCaseData = primaryCase->eclipseCaseData(); + auto primaryResultData = primaryCaseData->results( RiaDefines::PorosityModelType::MATRIX_MODEL ); - auto primaryCaseData = primaryCase->eclipseCaseData(); - auto primaryResultData = primaryCaseData->results( RiaDefines::PorosityModelType::MATRIX_MODEL ); - - ctx.sharedProjection = - std::make_unique( ctx.contourMapGrid.get(), primaryCaseData, primaryResultData ); - ctx.sharedProjection->generateGridMapping( ctx.resultAggregation, {}, ctx.kLayers, map->selectedPolygons() ); - } - else - { - RiaLogging::warning( "Formation names are missing for primary case, skipping statistics computation." ); - ctx.active = false; - } + ctx.sharedProjection = + std::make_unique( ctx.contourMapGrid.get(), primaryCaseData, primaryResultData ); + // With a data filter the mapping is generated per realization + if ( !map->m_dataFilter() ) ctx.sharedProjection->generateGridMapping( ctx.resultAggregation, {}, map->selectedPolygons() ); } } @@ -795,6 +767,15 @@ void RimStatisticsContourMap::computeStatisticsForMaps( const std::vectorupdateRealizationData( activeCellInfo, resultData ); + + // A data filter can accept different cells in each realization, e.g. a property filter or + // formation names defined per realization + if ( map->m_dataFilter() ) + { + applyDataFilterVisibility( *ctx.sharedProjection, map->m_dataFilter(), eCase ); + ctx.sharedProjection->generateGridMapping( ctx.resultAggregation, {}, map->selectedPolygons() ); + } + extractCaseResults( *ctx.sharedProjection, map->m_resultDefinition()->eclipseResultAddress(), map->m_resultDefinition()->hasDynamicResult(), @@ -805,23 +786,16 @@ void RimStatisticsContourMap::computeStatisticsForMaps( const std::vectorselectedFormations(), map->activeFormationNames() ) ) - { - RigEclipseContourMapProjection contourMapProjection( ctx.contourMapGrid.get(), eclipseCaseData, resultData ); - contourMapProjection.generateGridMapping( ctx.resultAggregation, {}, *kLayers, map->selectedPolygons() ); - extractCaseResults( contourMapProjection, - map->m_resultDefinition()->eclipseResultAddress(), - map->m_resultDefinition()->hasDynamicResult(), - ctx.resultAggregation, - ctx.floodSettings, - localToGlobalTimeSteps, - ctx.timestepResults ); - } - else - { - RiaLogging::warning( - std::format( "Formation names are missing for case {}, skipping case.", eCase->caseUserDescription() ) ); - } + RigEclipseContourMapProjection contourMapProjection( ctx.contourMapGrid.get(), eclipseCaseData, resultData ); + applyDataFilterVisibility( contourMapProjection, map->m_dataFilter(), eCase ); + contourMapProjection.generateGridMapping( ctx.resultAggregation, {}, map->selectedPolygons() ); + extractCaseResults( contourMapProjection, + map->m_resultDefinition()->eclipseResultAddress(), + map->m_resultDefinition()->hasDynamicResult(), + ctx.resultAggregation, + ctx.floodSettings, + localToGlobalTimeSteps, + ctx.timestepResults ); } } } @@ -948,15 +922,6 @@ std::vector> RimStatisticsContourMap::mapLocalToGlobalTimeSt return indexSubset; } -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::vector RimStatisticsContourMap::selectedFormations() const -{ - if ( !m_enableFormationFilter ) return {}; - return m_selectedFormations(); -} - //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -1050,9 +1015,8 @@ QString RimStatisticsContourMap::computeCacheValidityKey() const for ( int timeStep : selectedTimeSteps() ) parts << QString::number( timeStep ); - parts << ( m_enableFormationFilter() ? "formationFilter" : "noFormationFilter" ); - for ( const QString& formation : m_selectedFormations() ) - parts << formation; + // Include the filter configuration, so that editing the filter invalidates the cache + if ( m_dataFilter() ) parts << m_dataFilter()->writeObjectToXmlString(); for ( const auto& polygonLine : selectedPolygons() ) { diff --git a/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.h b/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.h index d6e0f783a5..b4f4b6f9d9 100644 --- a/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.h +++ b/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.h @@ -36,10 +36,10 @@ #include class RigContourMapGrid; +class RimCellFilter; class RimEclipseCase; class RimEclipseResultDefinition; class RimEclipseCaseEnsemble; -class RimFormationNames; class RimReservoirGridEnsemble; class RimEclipseContourMapView; class RimStatisticsContourMapView; @@ -98,7 +98,6 @@ public: std::vector selectedTimeStepDates() const; QString timeStepName( int timeStep ) const; - std::vector selectedFormations() const; std::vector> selectedPolygons() const; void switchToSelectedSourceCase(); @@ -132,7 +131,6 @@ private: void clearCacheFields(); static QString getCacheDirectoryPath(); - RimFormationNames* activeFormationNames() const; std::vector ensembleCases() const; std::set ensembleCasesInViews() const; @@ -143,10 +141,9 @@ private: caf::PdmField m_resultAggregation; caf::PdmField> m_selectedTimeSteps; caf::PdmChildField m_resultDefinition; - caf::PdmField m_enableFormationFilter; - caf::PdmField> m_selectedFormations; caf::PdmPtrField m_primaryCase; caf::PdmPtrArrayField m_selectedPolygons; + caf::PdmPtrField m_dataFilter; caf::PdmField> m_gridImportMode; caf::PdmField> m_resolution; diff --git a/ApplicationLibCode/ProjectDataModel/RimEclipseView.cpp b/ApplicationLibCode/ProjectDataModel/RimEclipseView.cpp index 6588aaf401..a85152f80f 100644 --- a/ApplicationLibCode/ProjectDataModel/RimEclipseView.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimEclipseView.cpp @@ -1377,6 +1377,9 @@ void RimEclipseView::initAfterRead() m_refinementRegions = new RimRefinementRegionCollection(); } + // Re-resolve the data filter source (case or ensemble) now that the view is attached to the tree. + propagateEclipseCaseToChildObjects(); + updateUiIconFromToggleField(); } diff --git a/ApplicationLibCode/ProjectDataModel/RimEclipseViewCollection.cpp b/ApplicationLibCode/ProjectDataModel/RimEclipseViewCollection.cpp index 496446835d..5903edb6b4 100644 --- a/ApplicationLibCode/ProjectDataModel/RimEclipseViewCollection.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimEclipseViewCollection.cpp @@ -91,6 +91,10 @@ RimEclipseView* RimEclipseViewCollection::addView( RimEclipseCase* eclipseCase ) { RimEclipseView* view = new RimEclipseView(); + // Parent before assigning the case: setEclipseCase() resolves the owning ensemble via the PDM + // ancestor chain, which only works once the view is attached to the tree. See #14710. + m_views.push_back( view ); + view->setEclipseCase( eclipseCase ); // Configure case provider callback @@ -105,8 +109,6 @@ RimEclipseView* RimEclipseViewCollection::addView( RimEclipseCase* eclipseCase ) caf::PdmDocument::updateUiIconStateRecursively( view ); - m_views.push_back( view ); - RimQuickAccessCollection::instance()->addQuickAccessFields( view ); view->loadDataAndUpdate(); diff --git a/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.cpp b/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.cpp index 167808e30e..722b9db97c 100644 --- a/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.cpp @@ -1099,6 +1099,9 @@ void RimReservoirGridEnsemble::loadGridDataFromFiles() updateGridModeToolTip(); updateStatisticsVisibility(); updateConnectedEditors(); + + // Re-initialize data filters now that the case data is loaded (needed for e.g. FORMATION_NAMES). See #14022. + if ( m_dataFilterCollection() ) m_dataFilterCollection->loadAndInitializeFilters(); } //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapCalculator.cpp b/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapCalculator.cpp index 080e5366d7..895bf7ae4a 100644 --- a/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapCalculator.cpp +++ b/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapCalculator.cpp @@ -309,7 +309,6 @@ std::vector>> const RigContourMapGrid& contourMapGrid, ResultAggregationType resultAggregation, const std::vector& weightingResultValues, - const std::set& kLayers, const std::vector>& limitToPolygons ) { int nCells = contourMapGrid.numberOfCells(); @@ -337,12 +336,12 @@ std::vector>> if ( isSummationResult ) { projected3dGridIndices[index] = - cellRayIntersectionAndResults( contourMapProjection, contourMapGrid, globalPos, weightingResultValues, kLayers ); + cellRayIntersectionAndResults( contourMapProjection, contourMapGrid, globalPos, weightingResultValues ); } else { projected3dGridIndices[index] = - cellOverlapVolumesAndResults( contourMapProjection, contourMapGrid, globalPos, weightingResultValues, kLayers ); + cellOverlapVolumesAndResults( contourMapProjection, contourMapGrid, globalPos, weightingResultValues ); } } return projected3dGridIndices; @@ -355,8 +354,7 @@ std::vector RigContourMapCalculator::cellOverlapVolumesAndResults( const RigContourMapProjection& contourMapProjection, const RigContourMapGrid& contourMapGrid, const cvf::Vec2d& globalPos2d, - const std::vector& weightingResultValues, - const std::set& kLayers ) + const std::vector& weightingResultValues ) { const cvf::BoundingBox& expandedBoundingBox = contourMapGrid.expandedBoundingBox(); cvf::Vec3d top2dElementCentroid( globalPos2d, expandedBoundingBox.max().z() ); @@ -390,11 +388,6 @@ std::vector { if ( cellGridIdxVisibility.isNull() || ( *cellGridIdxVisibility )[globalCellIdx] ) { - auto k = contourMapProjection.kLayer( globalCellIdx ); - if ( !kLayers.empty() ) - { - if ( !kLayers.contains( (int)k ) ) continue; - } kLayerCellIndexVector[contourMapProjection.kLayer( globalCellIdx )].push_back( globalCellIdx ); } } @@ -427,8 +420,7 @@ std::vector RigContourMapCalculator::cellRayIntersectionAndResults( const RigContourMapProjection& contourMapProjection, const RigContourMapGrid& contourMapGrid, const cvf::Vec2d& globalPos2d, - const std::vector& weightingResultValues, - const std::set& kLayers ) + const std::vector& weightingResultValues ) { std::vector> matchingVisibleCellsAndWeight; @@ -458,12 +450,7 @@ std::vector if ( cellGridIdxVisibility.isNull() || ( *cellGridIdxVisibility )[globalCellIdx] ) { - auto k = contourMapProjection.kLayer( globalCellIdx ); - if ( !kLayers.empty() ) - { - if ( !kLayers.contains( (int)k ) ) continue; - } - kLayerIndexMap[k].push_back( globalCellIdx ); + kLayerIndexMap[contourMapProjection.kLayer( globalCellIdx )].push_back( globalCellIdx ); } } diff --git a/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapCalculator.h b/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapCalculator.h index eb3636b88c..ee1845acb1 100644 --- a/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapCalculator.h +++ b/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapCalculator.h @@ -60,7 +60,6 @@ public: const RigContourMapGrid& contourMapGrid, ResultAggregationType resultAggregation, const std::vector& weightingResultValues, - const std::set& kLayers, const std::vector>& limitToPolygons ); static double calculateValueInMapCell( const RigContourMapProjection& contourMapProjection, @@ -71,14 +70,12 @@ public: static std::vector cellOverlapVolumesAndResults( const RigContourMapProjection& contourMapProjection, const RigContourMapGrid& contourMapGrid, const cvf::Vec2d& globalPos2d, - const std::vector& weightingResultValues, - const std::set& kLayers ); + const std::vector& weightingResultValues ); static std::vector cellRayIntersectionAndResults( const RigContourMapProjection& contourMapProjection, const RigContourMapGrid& contourMapGrid, const cvf::Vec2d& globalPos2d, - const std::vector& weightingResultValues, - const std::set& kLayers ); + const std::vector& weightingResultValues ); static bool isColumnResult( ResultAggregationType aggregationType ); static bool isMeanResult( ResultAggregationType aggregationType ); diff --git a/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapProjection.cpp b/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapProjection.cpp index 802aa1528a..6106440ea8 100644 --- a/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapProjection.cpp +++ b/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapProjection.cpp @@ -48,11 +48,10 @@ RigContourMapProjection::RigContourMapProjection( const RigContourMapGrid* conto std::vector>> RigContourMapProjection::generateGridMapping( RigContourMapCalculator::ResultAggregationType resultAggregation, const std::vector& weights, - const std::set& kLayers, const std::vector>& limitToPolygons ) { m_projected3dGridIndices = - RigContourMapCalculator::generateGridMapping( *this, *m_contourMapGrid, resultAggregation, weights, kLayers, limitToPolygons ); + RigContourMapCalculator::generateGridMapping( *this, *m_contourMapGrid, resultAggregation, weights, limitToPolygons ); return m_projected3dGridIndices; } diff --git a/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapProjection.h b/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapProjection.h index 051eaad122..84670b3860 100644 --- a/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapProjection.h +++ b/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapProjection.h @@ -92,7 +92,6 @@ public: virtual std::vector>> generateGridMapping( RigContourMapCalculator::ResultAggregationType resultAggregation, const std::vector& weights, - const std::set& kLayers, const std::vector>& limitToPolygons ); double interpolateValue( const cvf::Vec2d& gridPosition2d ) const; diff --git a/ApplicationLibCode/ReservoirDataModel/ContourMap/RigStatisticsContourMapProjection.cpp b/ApplicationLibCode/ReservoirDataModel/ContourMap/RigStatisticsContourMapProjection.cpp index ebc220e6c6..59eada38a5 100644 --- a/ApplicationLibCode/ReservoirDataModel/ContourMap/RigStatisticsContourMapProjection.cpp +++ b/ApplicationLibCode/ReservoirDataModel/ContourMap/RigStatisticsContourMapProjection.cpp @@ -44,7 +44,6 @@ RigStatisticsContourMapProjection::~RigStatisticsContourMapProjection() std::vector>> RigStatisticsContourMapProjection::generateGridMapping( RigContourMapCalculator::ResultAggregationType resultAggregation, const std::vector& weights, - const std::set& kLayers, const std::vector>& limitToPolygons ) { // The grid mapping is usually necessary to produce the data. For the statistics projection diff --git a/ApplicationLibCode/ReservoirDataModel/ContourMap/RigStatisticsContourMapProjection.h b/ApplicationLibCode/ReservoirDataModel/ContourMap/RigStatisticsContourMapProjection.h index d625ca18c4..e31dc4d255 100644 --- a/ApplicationLibCode/ReservoirDataModel/ContourMap/RigStatisticsContourMapProjection.h +++ b/ApplicationLibCode/ReservoirDataModel/ContourMap/RigStatisticsContourMapProjection.h @@ -42,7 +42,6 @@ public: std::vector>> generateGridMapping( RigContourMapCalculator::ResultAggregationType resultAggregation, const std::vector& weights, - const std::set& kLayers, const std::vector>& limitToPolygons ) override; std::vector getMapCellVisibility( int viewStepIndex, RigContourMapCalculator::ResultAggregationType resultAggregation ) override; diff --git a/ApplicationLibCode/ReservoirDataModel/RigFormationNames.cpp b/ApplicationLibCode/ReservoirDataModel/RigFormationNames.cpp index 98363251e8..caf4c5cdc2 100644 --- a/ApplicationLibCode/ReservoirDataModel/RigFormationNames.cpp +++ b/ApplicationLibCode/ReservoirDataModel/RigFormationNames.cpp @@ -167,22 +167,3 @@ void RigFormationNames::appendFormationRangeWithColor( const QString& name, cvf: m_formationColors.push_back( color ); } - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::set RigFormationNames::findKLayers( std::vector formationNames ) const -{ - std::set foundK; - - for ( int k = 1; k < (int)m_nameIndexPrKLayer.size(); k++ ) - { - auto layerName = formationNameFromKLayerIdx( k ); - if ( std::find( formationNames.begin(), formationNames.end(), layerName ) != formationNames.end() ) - { - foundK.insert( k ); - } - } - - return foundK; -} diff --git a/ApplicationLibCode/ReservoirDataModel/RigFormationNames.h b/ApplicationLibCode/ReservoirDataModel/RigFormationNames.h index 54424660fd..c589631019 100644 --- a/ApplicationLibCode/ReservoirDataModel/RigFormationNames.h +++ b/ApplicationLibCode/ReservoirDataModel/RigFormationNames.h @@ -20,7 +20,6 @@ #include -#include #include #include "cvfColor3.h" @@ -44,8 +43,6 @@ public: void appendFormationRange( const QString& name, cvf::Color3f color, int kStartIdx, int kEndIdx ); void appendFormationRangeHeight( const QString& name, cvf::Color3f color, int kLayerCount ); - std::set findKLayers( std::vector formationNames ) const; - private: static cvf::Color3f undefinedColor(); void appendFormationRangeWithColor( const QString& name, cvf::Color3f color, int kStartIdx, int kEndIdx ); diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index d705e2bad9..95340ae318 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -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 diff --git a/ApplicationLibCode/UnitTests/RimCellFilterTools-Test.cpp b/ApplicationLibCode/UnitTests/RimCellFilterTools-Test.cpp index 3145630d84..07ba144ef9 100644 --- a/ApplicationLibCode/UnitTests/RimCellFilterTools-Test.cpp +++ b/ApplicationLibCode/UnitTests/RimCellFilterTools-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 #include +#include #include #include +#include static std::unique_ptr 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(); + 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*>( propertyFilter.findField( "CategorySelection" ) ); + auto* selectedValuesField = dynamic_cast>*>( 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 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( { 0, 1 } ), visibleKLayers( visibilityA.p(), caseA.get() ) ); + EXPECT_EQ( std::set( { 0, 1, 2 } ), visibleKLayers( visibilityB.p(), caseB.get() ) ); +} diff --git a/ApplicationLibCode/UnitTests/RimDataFilterCollection-Test.cpp b/ApplicationLibCode/UnitTests/RimDataFilterCollection-Test.cpp index 932ab3c9b7..83d4026ef3 100644 --- a/ApplicationLibCode/UnitTests/RimDataFilterCollection-Test.cpp +++ b/ApplicationLibCode/UnitTests/RimDataFilterCollection-Test.cpp @@ -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(); + + 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(); + + 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(); + + 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() ); +} diff --git a/ApplicationLibCode/UnitTests/RimStatisticsContourMap-Test.cpp b/ApplicationLibCode/UnitTests/RimStatisticsContourMap-Test.cpp new file mode 100644 index 0000000000..c559f11f13 --- /dev/null +++ b/ApplicationLibCode/UnitTests/RimStatisticsContourMap-Test.cpp @@ -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 +// 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 +#include + +#include +#include + +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(); + eclipseCase->setCaseInfo( realizationFolder, filePath ); + if ( !eclipseCase->openEclipseGridFile() ) return nullptr; + + return eclipseCase.release(); +} + +std::unique_ptr createBruggeEnsemble( GridModeType gridMode ) +{ + auto ensemble = std::make_unique(); + + for ( const auto& [folder, fileName] : std::vector>{ { "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*>( ensemble->findField( "AutoDetectGridType" ) ); + auto* gridModeField = dynamic_cast>*>( 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>*>( map->findField( "ResultAggregation" ) ); + auto* resultDefinitionField = dynamic_cast*>( 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*>( 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( mainGrid->cellCountI() ); + rangeFilter->cellCountJ = static_cast( mainGrid->cellCountJ() ); + rangeFilter->cellCountK = 1; + + return rangeFilter; +} + +size_t countValidValues( const std::vector& values ) +{ + return std::count_if( values.begin(), values.end(), []( double v ) { return std::isfinite( v ); } ); +} + +size_t countDifferentValues( const std::vector& lhs, const std::vector& 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 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 ) ); +}