#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

@@ -1057,6 +1057,10 @@ void RifReaderEclipseOutput::sourSimRlResult( const QString& result, size_t step
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
std::vector<QDateTime> RifReaderEclipseOutput::allTimeSteps() const std::vector<QDateTime> RifReaderEclipseOutput::allTimeSteps() const
{ {
// allTimeSteps() is also used to discover the time steps a case has before any dynamic result has actually
// been requested, so the underlying restart file access must be created here rather than assumed present.
const_cast<RifReaderEclipseOutput*>( this )->ensureDynamicResultAccessIsPresent();
std::vector<QDateTime> steps; std::vector<QDateTime> steps;
if ( m_dynamicResultsAccess.notNull() ) if ( m_dynamicResultsAccess.notNull() )
{ {
@@ -19,12 +19,16 @@
#include "RimCellFilterTools.h" #include "RimCellFilterTools.h"
#include "RigEclipseCaseData.h" #include "RigEclipseCaseData.h"
#include "RigEclipseResultAddress.h"
#include "RigGridBase.h" #include "RigGridBase.h"
#include "RigLocalGrid.h" #include "RigLocalGrid.h"
#include "RigMainGrid.h" #include "RigMainGrid.h"
#include "RimCellFilter.h" #include "RimCellFilter.h"
#include "RimCombinedFilter.h"
#include "RimEclipseCase.h" #include "RimEclipseCase.h"
#include "RimEclipsePropertyFilter.h"
#include "RimEclipseResultDefinition.h"
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
/// Evaluate the filter against the given case for all grids, and return the visibility indexed by /// Evaluate the filter against the given case for all grids, and return the visibility indexed by
@@ -105,3 +109,56 @@ cvf::ref<cvf::UByteArray>
return reservoirVisibility; return reservoirVisibility;
} }
//--------------------------------------------------------------------------------------------------
/// A property filter on a dynamic result (e.g. SOIL, SWAT) accepts different cells at each time step.
/// A combined filter is dynamic if any of its descendants is. Range, index, polygon and formation
/// name filters are static: they select the same cells regardless of time step.
//--------------------------------------------------------------------------------------------------
bool RimCellFilterTools::isDynamicFilter( const RimCellFilter* filter )
{
if ( auto* propertyFilter = dynamic_cast<const RimEclipsePropertyFilter*>( filter ) )
{
if ( !propertyFilter->resultDefinition() ) return false;
// Classify by result category rather than hasDynamicResult(), which only answers correctly once
// the result has been loaded. Ensemble data filters may not have loaded their result yet.
switch ( propertyFilter->resultDefinition()->resultType() )
{
case RiaDefines::ResultCatType::DYNAMIC_NATIVE:
case RiaDefines::ResultCatType::SOURSIMRL:
case RiaDefines::ResultCatType::FLOW_DIAGNOSTICS:
case RiaDefines::ResultCatType::INJECTION_FLOODING:
return true;
default:
return propertyFilter->resultDefinition()->hasDynamicResult();
}
}
else if ( auto* combinedFilter = dynamic_cast<const RimCombinedFilter*>( filter ) )
{
return combinedFilter->hasActiveDynamicPropertyDescendant();
}
return false;
}
//--------------------------------------------------------------------------------------------------
/// The result address a dynamic property filter (or one of a combined filter's descendants) is based
/// on, so callers can make sure it is loaded before asking the case how many time steps it has.
//--------------------------------------------------------------------------------------------------
std::optional<RigEclipseResultAddress> RimCellFilterTools::dynamicResultAddress( const RimCellFilter* filter )
{
if ( auto* propertyFilter = dynamic_cast<const RimEclipsePropertyFilter*>( filter ) )
{
if ( isDynamicFilter( propertyFilter ) ) return propertyFilter->resultDefinition()->eclipseResultAddress();
}
else if ( auto* combinedFilter = dynamic_cast<const RimCombinedFilter*>( filter ) )
{
for ( RimCellFilter* child : combinedFilter->filters() )
{
if ( auto address = dynamicResultAddress( child ) ) return address;
}
}
return std::nullopt;
}
@@ -21,6 +21,9 @@
#include "cvfArray.h" #include "cvfArray.h"
#include "cvfObject.h" #include "cvfObject.h"
#include <optional>
class RigEclipseResultAddress;
class RimCellFilter; class RimCellFilter;
class RimEclipseCase; class RimEclipseCase;
@@ -31,4 +34,12 @@ class RimCellFilterTools
{ {
public: public:
static cvf::ref<cvf::UByteArray> computeReservoirCellVisibility( RimCellFilter* filter, RimEclipseCase* eclipseCase, size_t timeStepIndex ); static cvf::ref<cvf::UByteArray> computeReservoirCellVisibility( RimCellFilter* filter, RimEclipseCase* eclipseCase, size_t timeStepIndex );
// True if the filter's visible cells can change from one time step to the next, i.e. it is (or
// contains) a property filter on a dynamic (time-varying) result such as SOIL or SWAT.
static bool isDynamicFilter( const RimCellFilter* filter );
// The dynamic result address the filter (or one of its descendants) is based on, if any. Used to make sure
// that result is loaded before its time step count is queried.
static std::optional<RigEclipseResultAddress> dynamicResultAddress( const RimCellFilter* filter );
}; };
@@ -81,29 +81,44 @@ namespace
{ {
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
/// Restrict the grid mapping to the cells accepted by the data filter, evaluated against the given /// 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 /// realization at the given (local) time step.
/// time steps.
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
void applyDataFilterVisibility( RigEclipseContourMapProjection& projection, RimCellFilter* dataFilter, RimEclipseCase* eCase ) void applyDataFilterVisibility( RigEclipseContourMapProjection& projection, RimCellFilter* dataFilter, RimEclipseCase* eCase, size_t timeStepIndex )
{ {
if ( !dataFilter ) return; if ( !dataFilter ) return;
projection.setCellVisibility( RimCellFilterTools::computeReservoirCellVisibility( dataFilter, eCase, 0 ) ); projection.setCellVisibility( RimCellFilterTools::computeReservoirCellVisibility( dataFilter, eCase, timeStepIndex ) );
} }
//--------------------------------------------------------------------------------------------------
/// Generate one result per selected time step if the mapped property is dynamic or the active data
/// filter is dynamic (its visible cells then differ per time step, even for a static property).
/// Otherwise generate a single, time-independent result.
//--------------------------------------------------------------------------------------------------
void extractCaseResults( RigEclipseContourMapProjection& projection, void extractCaseResults( RigEclipseContourMapProjection& projection,
RimCellFilter* dataFilter,
RimEclipseCase* eCase,
const RigEclipseResultAddress& resultAddress, const RigEclipseResultAddress& resultAddress,
bool hasDynamicResult, bool hasDynamicResult,
bool hasDynamicFilter,
RigContourMapCalculator::ResultAggregationType resultAggregation, RigContourMapCalculator::ResultAggregationType resultAggregation,
RigFloodingSettings& floodSettings, RigFloodingSettings& floodSettings,
const std::vector<std::vector<cvf::Vec3d>>& selectedPolygons,
const std::vector<std::pair<int, int>>& localToGlobalTimeSteps, const std::vector<std::pair<int, int>>& localToGlobalTimeSteps,
std::map<size_t, std::vector<std::vector<double>>>& timestepResults ) std::map<size_t, std::vector<std::vector<double>>>& timestepResults )
{ {
if ( hasDynamicResult ) if ( hasDynamicResult || hasDynamicFilter )
{ {
for ( auto [localTs, globalTs] : localToGlobalTimeSteps ) for ( auto [localTs, globalTs] : localToGlobalTimeSteps )
{ {
timestepResults[globalTs].push_back( projection.generateResults( resultAddress, resultAggregation, localTs, floodSettings ) ); if ( hasDynamicFilter )
{
applyDataFilterVisibility( projection, dataFilter, eCase, static_cast<size_t>( localTs ) );
projection.generateGridMapping( resultAggregation, {}, selectedPolygons );
}
const int resultTimeStep = hasDynamicResult ? localTs : 0;
timestepResults[globalTs].push_back( projection.generateResults( resultAddress, resultAggregation, resultTimeStep, floodSettings ) );
} }
} }
else else
@@ -751,6 +766,24 @@ void RimStatisticsContourMap::computeStatisticsForMaps( const std::vector<RimSta
auto activeCellInfo = eclipseCaseData->activeCellInfo( RiaDefines::PorosityModelType::MATRIX_MODEL ); auto activeCellInfo = eclipseCaseData->activeCellInfo( RiaDefines::PorosityModelType::MATRIX_MODEL );
auto resultData = eclipseCaseData->results( RiaDefines::PorosityModelType::MATRIX_MODEL ); auto resultData = eclipseCaseData->results( RiaDefines::PorosityModelType::MATRIX_MODEL );
// Make sure at least one dynamic result this case needs is loaded before asking for its time step
// dates: allTimeStepDatesFromEclipseReader() and the loaded-result based fallback below both only
// report the full time step count once such a result is known.
for ( auto& ctx : contexts )
{
if ( !ctx.active ) continue;
if ( ctx.map->m_resultDefinition()->hasDynamicResult() )
resultData->ensureKnownResultLoaded( ctx.map->m_resultDefinition()->eclipseResultAddress() );
if ( auto address = RimCellFilterTools::dynamicResultAddress( ctx.map->m_dataFilter() ) )
resultData->ensureKnownResultLoaded( *address );
}
// The case's own time step dates, read directly from the reader rather than from already-loaded
// results: a realization case may not have any dynamic result loaded yet, in which case
// eCase->timeStepDates() would incorrectly report only a single (or zero) time step.
auto caseTimeStepDates = resultData->allTimeStepDatesFromEclipseReader( eCase->gridFileName() );
if ( caseTimeStepDates.empty() ) caseTimeStepDates = eCase->timeStepDates();
// Prefetch every dynamic result time step this case needs before reading any of it for real. // Prefetch every dynamic result time step this case needs before reading any of it for real.
// This case has no view of its own, so it is the only chance a cloud-backed reader gets to // This case has no view of its own, so it is the only chance a cloud-backed reader gets to
// fetch exactly the time steps needed before the case is closed again - see // fetch exactly the time steps needed before the case is closed again - see
@@ -762,12 +795,15 @@ void RimStatisticsContourMap::computeStatisticsForMaps( const std::vector<RimSta
std::map<QString, std::vector<size_t>> stepsToPrefetchByResult; std::map<QString, std::vector<size_t>> stepsToPrefetchByResult;
for ( auto& ctx : contexts ) for ( auto& ctx : contexts )
{ {
if ( !ctx.active || !ctx.map->m_resultDefinition()->hasDynamicResult() ) continue; if ( !ctx.active ) continue;
auto& steps = stepsToPrefetchByResult[ctx.map->m_resultDefinition()->eclipseResultAddress().resultName()]; auto localToGlobalTimeSteps = ctx.map->mapLocalToGlobalTimeSteps( caseTimeStepDates );
for ( auto [localTs, globalTs] : ctx.map->mapLocalToGlobalTimeSteps( eCase->timeStepDates() ) )
if ( ctx.map->m_resultDefinition()->hasDynamicResult() )
{ {
steps.push_back( static_cast<size_t>( localTs ) ); auto& steps = stepsToPrefetchByResult[ctx.map->m_resultDefinition()->eclipseResultAddress().resultName()];
for ( auto [localTs, globalTs] : localToGlobalTimeSteps )
steps.push_back( static_cast<size_t>( localTs ) );
} }
} }
@@ -784,38 +820,52 @@ void RimStatisticsContourMap::computeStatisticsForMaps( const std::vector<RimSta
if ( !ctx.active ) continue; if ( !ctx.active ) continue;
RimStatisticsContourMap* map = ctx.map; RimStatisticsContourMap* map = ctx.map;
auto localToGlobalTimeSteps = map->mapLocalToGlobalTimeSteps( eCase->timeStepDates() ); auto localToGlobalTimeSteps = map->mapLocalToGlobalTimeSteps( caseTimeStepDates );
if ( ctx.useSharedGrid ) if ( ctx.useSharedGrid )
{ {
ctx.sharedProjection->updateRealizationData( activeCellInfo, resultData ); ctx.sharedProjection->updateRealizationData( activeCellInfo, resultData );
// A data filter can accept different cells in each realization, e.g. a property filter or // A data filter can accept different cells in each realization, e.g. a property filter or
// formation names defined per realization // formation names defined per realization. A dynamic filter's visible cells also change per
if ( map->m_dataFilter() ) // time step, so its mapping is (re)generated per time step inside extractCaseResults() instead.
const bool filterIsDynamic = RimCellFilterTools::isDynamicFilter( map->m_dataFilter() );
if ( map->m_dataFilter() && !filterIsDynamic )
{ {
applyDataFilterVisibility( *ctx.sharedProjection, map->m_dataFilter(), eCase ); applyDataFilterVisibility( *ctx.sharedProjection, map->m_dataFilter(), eCase, 0 );
ctx.sharedProjection->generateGridMapping( ctx.resultAggregation, {}, map->selectedPolygons() ); ctx.sharedProjection->generateGridMapping( ctx.resultAggregation, {}, map->selectedPolygons() );
} }
extractCaseResults( *ctx.sharedProjection, extractCaseResults( *ctx.sharedProjection,
map->m_dataFilter(),
eCase,
map->m_resultDefinition()->eclipseResultAddress(), map->m_resultDefinition()->eclipseResultAddress(),
map->m_resultDefinition()->hasDynamicResult(), map->m_resultDefinition()->hasDynamicResult(),
filterIsDynamic,
ctx.resultAggregation, ctx.resultAggregation,
ctx.floodSettings, ctx.floodSettings,
map->selectedPolygons(),
localToGlobalTimeSteps, localToGlobalTimeSteps,
ctx.timestepResults ); ctx.timestepResults );
} }
else else
{ {
RigEclipseContourMapProjection contourMapProjection( ctx.contourMapGrid.get(), eclipseCaseData, resultData ); RigEclipseContourMapProjection contourMapProjection( ctx.contourMapGrid.get(), eclipseCaseData, resultData );
applyDataFilterVisibility( contourMapProjection, map->m_dataFilter(), eCase ); const bool filterIsDynamic = RimCellFilterTools::isDynamicFilter( map->m_dataFilter() );
contourMapProjection.generateGridMapping( ctx.resultAggregation, {}, map->selectedPolygons() ); if ( !filterIsDynamic )
{
applyDataFilterVisibility( contourMapProjection, map->m_dataFilter(), eCase, 0 );
contourMapProjection.generateGridMapping( ctx.resultAggregation, {}, map->selectedPolygons() );
}
extractCaseResults( contourMapProjection, extractCaseResults( contourMapProjection,
map->m_dataFilter(),
eCase,
map->m_resultDefinition()->eclipseResultAddress(), map->m_resultDefinition()->eclipseResultAddress(),
map->m_resultDefinition()->hasDynamicResult(), map->m_resultDefinition()->hasDynamicResult(),
filterIsDynamic,
ctx.resultAggregation, ctx.resultAggregation,
ctx.floodSettings, ctx.floodSettings,
map->selectedPolygons(),
localToGlobalTimeSteps, localToGlobalTimeSteps,
ctx.timestepResults ); ctx.timestepResults );
} }
@@ -889,11 +939,13 @@ std::vector<double> RimStatisticsContourMap::result( size_t timeStep, Statistics
} }
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
/// /// A single, time-independent result is enough when both the mapped property and the active data
/// filter are static. Otherwise the result (or the filter's visible cells) can differ per time step,
/// so fall back to the user-selected time steps.
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
std::vector<int> RimStatisticsContourMap::selectedTimeSteps() const std::vector<int> RimStatisticsContourMap::selectedTimeSteps() const
{ {
if ( !m_resultDefinition->hasDynamicResult() ) if ( !m_resultDefinition->hasDynamicResult() && !RimCellFilterTools::isDynamicFilter( m_dataFilter() ) )
{ {
return { 0 }; return { 0 };
} }
@@ -913,7 +965,12 @@ std::vector<QDateTime> RimStatisticsContourMap::selectedTimeStepDates() const
auto eCase = eclipseCase(); auto eCase = eclipseCase();
if ( eCase != nullptr ) if ( eCase != nullptr )
{ {
auto allDates = eCase->timeStepDates(); // Read time steps directly from the reader rather than from already-loaded results: the primary case may
// not have any dynamic result loaded yet, in which case eCase->timeStepDates() would under-report them.
auto* resultData = eCase->results( RiaDefines::PorosityModelType::MATRIX_MODEL );
auto allDates = resultData ? resultData->allTimeStepDatesFromEclipseReader( eCase->gridFileName() ) : std::vector<QDateTime>();
if ( allDates.empty() ) allDates = eCase->timeStepDates();
for ( auto i : selectedTimeSteps() ) for ( auto i : selectedTimeSteps() )
{ {
if ( i < (int)allDates.size() ) retDates.push_back( allDates[i] ); if ( i < (int)allDates.size() ) retDates.push_back( allDates[i] );
@@ -28,6 +28,7 @@
#include "RiaResultNames.h" #include "RiaResultNames.h"
#include "RifReaderEclipseOutput.h" #include "RifReaderEclipseOutput.h"
#include "RifReaderOpmCommon.h"
#include "RigAllanDiagramData.h" #include "RigAllanDiagramData.h"
#include "RigAllanUtil.h" #include "RigAllanUtil.h"
@@ -569,19 +570,23 @@ bool RigCaseCellResultsData::hasFlowDiagUsableFluxes() const
} }
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
/// /// Time steps reported by the file reader itself, independent of which results (if any) have been
/// loaded so far. gridFileName is only needed for the opm-common reader, which requires it to locate
/// the matching restart file.
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
std::vector<QDateTime> RigCaseCellResultsData::allTimeStepDatesFromEclipseReader() const std::vector<QDateTime> RigCaseCellResultsData::allTimeStepDatesFromEclipseReader( const QString& gridFileName ) const
{ {
const RifReaderEclipseOutput* rifReaderOutput = dynamic_cast<const RifReaderEclipseOutput*>( m_readerInterface.p() ); if ( const RifReaderEclipseOutput* rifReaderOutput = dynamic_cast<const RifReaderEclipseOutput*>( m_readerInterface.p() ) )
if ( rifReaderOutput )
{ {
return rifReaderOutput->allTimeSteps(); return rifReaderOutput->allTimeSteps();
} }
else
if ( auto* rifReaderOpmCommon = dynamic_cast<const RifReaderOpmCommon*>( m_readerInterface.p() ) )
{ {
return std::vector<QDateTime>(); if ( !gridFileName.isEmpty() ) return const_cast<RifReaderOpmCommon*>( rifReaderOpmCommon )->timeStepsOnFile( gridFileName );
} }
return std::vector<QDateTime>();
} }
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
@@ -107,7 +107,7 @@ public:
size_t timeStepCount( const RigEclipseResultAddress& resVarAddr ) const; size_t timeStepCount( const RigEclipseResultAddress& resVarAddr ) const;
size_t maxTimeStepCount( RigEclipseResultAddress* resultAddressWithMostTimeSteps = nullptr ) const; size_t maxTimeStepCount( RigEclipseResultAddress* resultAddressWithMostTimeSteps = nullptr ) const;
std::vector<QDateTime> allTimeStepDatesFromEclipseReader() const; std::vector<QDateTime> allTimeStepDatesFromEclipseReader( const QString& gridFileName = QString() ) const;
std::vector<QDateTime> timeStepDates() const; std::vector<QDateTime> timeStepDates() const;
std::vector<QDateTime> timeStepDates( const RigEclipseResultAddress& resVarAddr ) const; std::vector<QDateTime> timeStepDates( const RigEclipseResultAddress& resVarAddr ) const;
std::vector<double> daysSinceSimulationStart() const; std::vector<double> daysSinceSimulationStart() const;
@@ -24,12 +24,16 @@
#include "ContourMap/RigContourMapCalculator.h" #include "ContourMap/RigContourMapCalculator.h"
#include "ContourMap/RigContourMapGrid.h" #include "ContourMap/RigContourMapGrid.h"
#include "RigCaseCellResultsData.h"
#include "RigEclipseCaseData.h" #include "RigEclipseCaseData.h"
#include "RigEclipseResultAddress.h"
#include "RigMainGrid.h" #include "RigMainGrid.h"
#include "ContourMap/RimStatisticsContourMap.h" #include "ContourMap/RimStatisticsContourMap.h"
#include "RiaResultNames.h"
#include "RimCellRangeFilter.h" #include "RimCellRangeFilter.h"
#include "RimDataFilterCollection.h" #include "RimDataFilterCollection.h"
#include "RimEclipsePropertyFilter.h"
#include "RimEclipseResultCase.h" #include "RimEclipseResultCase.h"
#include "RimEclipseResultDefinition.h" #include "RimEclipseResultDefinition.h"
#include "RimProject.h" #include "RimProject.h"
@@ -91,6 +95,43 @@ std::unique_ptr<RimReservoirGridEnsemble> createBruggeEnsemble( GridModeType gri
return ensemble; 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 ) RimStatisticsContourMap* addPoroContourMap( RimReservoirGridEnsemble* ensemble, GridImportMode importMode )
{ {
auto* map = new RimStatisticsContourMap; auto* map = new RimStatisticsContourMap;
@@ -229,6 +270,53 @@ TEST( RimStatisticsContourMapTest, DataFilterSharedAndIndividualGridsAgree )
EXPECT_EQ( 0u, countDifferentValues( shared, individual ) ); 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 namespace
{ {
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------