#14710 Compute per-time-step visible cells for dynamic data filters

A static property with an active dynamic filter now produces a contour map per time step. Adds a synthetic ensemble fixture with unified restart files, as the OPM reader used for statistics does not support per-step restart files.
This commit is contained in:
Magne Sjaastad committed 2026-09-28 10:30:12 +02:00
1 parent cf75dbf58e
commit bb8af7bbbd
19 files changed
+248 -26

No files matched your search

@@ -24,12 +24,16 @@
#include "ContourMap/RigContourMapCalculator.h"
#include "ContourMap/RigContourMapGrid.h"
#include "RigCaseCellResultsData.h"
#include "RigEclipseCaseData.h"
#include "RigEclipseResultAddress.h"
#include "RigMainGrid.h"
#include "ContourMap/RimStatisticsContourMap.h"
#include "RiaResultNames.h"
#include "RimCellRangeFilter.h"
#include "RimDataFilterCollection.h"
#include "RimEclipsePropertyFilter.h"
#include "RimEclipseResultCase.h"
#include "RimEclipseResultDefinition.h"
#include "RimProject.h"
@@ -91,6 +95,43 @@ std::unique_ptr<RimReservoirGridEnsemble> createBruggeEnsemble( GridModeType gri
return ensemble;
}
// "synthetic_ensemble_with_dynamic_filter" has unified .UNRST restart files (unlike Case_with_10_timesteps,
// which uses the legacy per-step .X#### format). Computing statistics forces the OPM_COMMON grid reader,
// which only supports unified restart files, so a dynamic (e.g. SWAT) filter needs this fixture instead.
// Real1-3 have their PORO and SWAT values scaled by a distinct per-realization factor relative to Real0,
// so the ensemble also exercises real inter-realization variability, not just identical copies.
std::unique_ptr<RimReservoirGridEnsemble> createSyntheticDynamicFilterEnsemble()
{
auto ensemble = std::make_unique<RimReservoirGridEnsemble>();
QDir baseFolder( TEST_MODEL_DIR );
if ( !baseFolder.cd( "synthetic_ensemble_with_dynamic_filter" ) ) return nullptr;
for ( const auto& folder : { "Real0", "Real1", "Real2", "Real3" } )
{
QDir realizationFolder( baseFolder );
if ( !realizationFolder.cd( folder ) ) return nullptr;
QString filePath = realizationFolder.absoluteFilePath( "SYNTH.EGRID" );
if ( !QFile::exists( filePath ) ) return nullptr;
auto eclipseCase = std::make_unique<RimEclipseResultCase>();
eclipseCase->setCaseInfo( folder, filePath );
if ( !eclipseCase->openEclipseGridFile() ) return nullptr;
ensemble->addCase( eclipseCase.release() );
}
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( GridModeType::SHARED_GRID );
return ensemble;
}
RimStatisticsContourMap* addPoroContourMap( RimReservoirGridEnsemble* ensemble, GridImportMode importMode )
{
auto* map = new RimStatisticsContourMap;
@@ -229,6 +270,53 @@ TEST( RimStatisticsContourMapTest, DataFilterSharedAndIndividualGridsAgree )
EXPECT_EQ( 0u, countDifferentValues( shared, individual ) );
}
//--------------------------------------------------------------------------------------------------
/// A static mapped property (PORO) with an active dynamic filter (SWAT) must still produce a result
/// per selected time step, since the filter's visible cells change over time even if PORO does not.
//--------------------------------------------------------------------------------------------------
TEST( RimStatisticsContourMapTest, StaticResultWithDynamicFilterProducesResultsPerTimeStep )
{
auto ensemble = createSyntheticDynamicFilterEnsemble();
ASSERT_TRUE( ensemble != nullptr );
auto* map = addPoroContourMap( ensemble.get(), GridImportMode::SHARED_GRID );
ASSERT_TRUE( map != nullptr );
// The synthetic fixture's unified restart file has 13 time steps (indices 0..12)
const int lastTimeStep = 12;
auto* swatFilter = ensemble->dataFilterCollection()->addNewPropertyFilter();
swatFilter->resultDefinition()->setResultType( RiaDefines::ResultCatType::DYNAMIC_NATIVE );
swatFilter->resultDefinition()->setResultVariable( RiaResultNames::swat() );
auto* lowerField = dynamic_cast<caf::PdmField<double>*>( swatFilter->findField( "LowerBound" ) );
auto* upperField = dynamic_cast<caf::PdmField<double>*>( swatFilter->findField( "UpperBound" ) );
ASSERT_TRUE( lowerField && upperField );
lowerField->setValue( 0.25 );
upperField->setValue( 0.255 );
setDataFilter( map, swatFilter );
auto* selectedTimeStepsField = dynamic_cast<caf::PdmField<std::vector<int>>*>( map->findField( "SelectedTimeSteps" ) );
ASSERT_TRUE( selectedTimeStepsField != nullptr );
selectedTimeStepsField->setValue( { 0, lastTimeStep } );
// A dynamic filter must make selectedTimeSteps() return the full user selection, even for static PORO
EXPECT_EQ( ( std::vector<int>{ 0, lastTimeStep } ), map->selectedTimeSteps() );
RimStatisticsContourMap::computeStatisticsForMaps( { map } );
const auto resultAtFirstStep = map->result( 0, RimStatisticsContourMap::StatisticsType::MEAN );
const auto resultAtLastStep = map->result( 1, RimStatisticsContourMap::StatisticsType::MEAN );
EXPECT_GT( countValidValues( resultAtFirstStep ), 0u );
EXPECT_GT( countValidValues( resultAtLastStep ), 0u );
ASSERT_EQ( resultAtFirstStep.size(), resultAtLastStep.size() );
// The filter's visible cells differ per time step (SWAT changes), so the aggregated PORO results differ too
EXPECT_GT( countDifferentValues( resultAtFirstStep, resultAtLastStep ), 0u );
}
namespace
{
//--------------------------------------------------------------------------------------------------