Fix ensemble contour map GENERATED result statistics and caching bugs (#14813)

Fixes a set of related bugs affecting ensemble contour maps that use a GENERATED (grid calculation) result, plus a related bug where switching a view's case lost generated results.
This commit is contained in:
Magne Sjaastad authored and GitHub committed 2026-10-02 15:08:30 +02:00
1 parent 1cfe49262a
commit c5c0e05012
8 files changed
+526 -8

No files matched your search

@@ -50,6 +50,8 @@
#include "RimEclipseContourMapProjection.h"
#include "RimEclipseResultCase.h"
#include "RimEclipseResultDefinition.h"
#include "RimGridCalculation.h"
#include "RimGridCalculationCollection.h"
#include "RimProject.h"
#include "RimReservoirGridEnsemble.h"
#include "RimSimWellInViewCollection.h"
@@ -90,6 +92,23 @@ void applyDataFilterVisibility( RigEclipseContourMapProjection& projection, RimC
projection.setCellVisibility( RimCellFilterTools::computeReservoirCellVisibility( dataFilter, eCase, timeStepIndex ) );
}
//--------------------------------------------------------------------------------------------------
/// A GENERATED result only exists in memory, so it must be recomputed for each realization while it
/// is open here, as it is otherwise lost when the case is closed again.
//--------------------------------------------------------------------------------------------------
void ensureGeneratedResultIsComputed( const RimEclipseResultDefinition* resultDefinition, RimEclipseCase* eCase )
{
if ( !resultDefinition || resultDefinition->resultType() != RiaDefines::ResultCatType::GENERATED ) return;
auto project = RimProject::current();
if ( !project ) return;
RimGridCalculation* calculation = project->gridCalculationCollection()->findCalculation( resultDefinition->resultVariable() );
if ( !calculation ) return;
calculation->calculateForCase( eCase );
}
//--------------------------------------------------------------------------------------------------
/// 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).
@@ -775,6 +794,13 @@ void RimStatisticsContourMap::computeStatisticsForMaps( const std::vector<RimSta
auto activeCellInfo = eclipseCaseData->activeCellInfo( RiaDefines::PorosityModelType::MATRIX_MODEL );
auto resultData = eclipseCaseData->results( RiaDefines::PorosityModelType::MATRIX_MODEL );
// GENERATED results are not stored on disk, so recompute them for this realization while it is open.
for ( auto& ctx : contexts )
{
if ( !ctx.active ) continue;
ensureGeneratedResultIsComputed( ctx.map->m_resultDefinition(), eCase );
}
// 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.
@@ -1062,6 +1088,14 @@ void RimStatisticsContourMap::ensureResultsComputed()
// Use the Compute button to force a recomputation after changing settings.
if ( m_contourMapGrid ) return;
// A GENERATED result only reports as dynamic once computed, and the disk cache key depends on that
// via selectedTimeSteps(). Ensure the result is computed for the primary case before checking the
// cache, so the key is stable between save and load.
if ( RimEclipseCase* primaryCase = eclipseCase() )
{
if ( primaryCase->ensureReservoirCaseIsOpen() ) ensureGeneratedResultIsComputed( m_resultDefinition(), primaryCase );
}
if ( loadCachedResults() ) return;
// Compute all pending sibling contour maps in the same sweep over the ensemble realizations, so
@@ -77,6 +77,7 @@
#include "RimFracture.h"
#include "RimFractureTemplateCollection.h"
#include "RimGeoMechResultDefinition.h"
#include "RimGridCalculationCollection.h"
#include "RimGridCollection.h"
#include "RimGridCrossPlotDataSet.h"
#include "RimGridView.h"
@@ -533,6 +534,13 @@ void RimEclipseView::fieldChangedByUi( const caf::PdmFieldHandle* changedField,
previousSumoCase->cancelPendingTransfers();
}
// Grid calculation results are not stored with the case, so compute them before the result
// definitions are updated, otherwise generated results are missing and the view is cleared.
if ( auto project = RimProject::current(); project && project->gridCalculationCollection() )
{
project->gridCalculationCollection()->ensureCalculationsAreComputed( m_eclipseCase );
}
propagateEclipseCaseToChildObjects();
// Sync with RimReloadCaseTools::updateAll3dViews
@@ -227,7 +227,9 @@ RimGridCalculationVariable* RimGridCalculation::createVariable()
//--------------------------------------------------------------------------------------------------
bool RimGridCalculation::calculate()
{
CloseCasesOpenedForCalculation closeCasesWhenFinished( sourceAndDestinationCases( *this, outputEclipseCases() ) );
const std::vector<RimEclipseCase*> calculationCases = casesToCalculate();
CloseCasesOpenedForCalculation closeCasesWhenFinished( sourceAndDestinationCases( *this, calculationCases ) );
const bool useCellFilterView = ( m_filterType() == FilterType::CELL_FILTER_VIEW ) && m_cellFilterView() != nullptr;
const bool useDataFilter = ( m_filterType() == FilterType::DATA_FILTER );
@@ -250,7 +252,7 @@ bool RimGridCalculation::calculate()
if ( inputCase && !inputCase->eclipseCaseData() ) inputCase->ensureReservoirCaseIsOpen();
}
for ( auto calculationCase : outputEclipseCases() )
for ( auto calculationCase : calculationCases )
{
if ( !calculationCase ) continue;
@@ -336,8 +338,64 @@ bool RimGridCalculation::calculate()
timeSteps = tmp;
}
bool evaluateDependentCalculations = true;
const bool ok = calculateForCases( calculationCases, inputValueVisibilityFilter.p(), timeSteps, evaluateDependentCalculations );
// When only the ensemble's main case was (re)computed above, other already open realizations keep their
// previously computed (now potentially stale) result. Clear it, so it is lazily recomputed with the
// current expression/filter next time it is needed, instead of being mistaken for up to date.
if ( ok && calculateForEnsemble && !isAggregationExpression() )
{
for ( RimEclipseCase* otherCase : outputEclipseCases() )
{
if ( !otherCase || !otherCase->eclipseCaseData() ) continue;
if ( std::find( calculationCases.begin(), calculationCases.end(), otherCase ) != calculationCases.end() ) continue;
otherCase->results( RiaDefines::PorosityModelType::MATRIX_MODEL )->clearScalarResult( outputAddress() );
}
}
return ok;
}
//--------------------------------------------------------------------------------------------------
/// Compute the result for a single case, e.g. when a view is stepped to another case. The case does not
/// have to be an output case, as variables referring to the destination case are evaluated on the case.
//--------------------------------------------------------------------------------------------------
bool RimGridCalculation::calculateForCase( RimEclipseCase* eclipseCase )
{
// Source cases are not necessarily open (e.g. after reopening a project), and validateVariables()
// requires an open case for every variable. Open them here, as calculate() does.
for ( auto inputCase : inputCases() )
{
if ( !inputCase ) continue;
if ( !inputCase->eclipseCaseData() ) inputCase->ensureReservoirCaseIsOpen();
if ( !inputCase->eclipseCaseData() ) return false;
}
// Values from other cases and the cell filter view are mapped cell by cell, requiring equal grid sizes
const bool calculateForEnsemble = m_destinationEnsemble() || m_additionalCasesType == AdditionalCasesType::ENSEMBLE;
for ( auto inputCase : inputCases() )
{
if ( calculateForEnsemble || inputCase == destinationCase() ) continue;
if ( !eclipseCase->isGridSizeEqualTo( inputCase ) ) return false;
}
cvf::ref<cvf::UByteArray> inputValueVisibilityFilter;
if ( m_filterType() == FilterType::CELL_FILTER_VIEW && m_cellFilterView() )
{
if ( !eclipseCase->isGridSizeEqualTo( dynamic_cast<RimEclipseCase*>( m_cellFilterView()->ownerCase() ) ) ) return false;
inputValueVisibilityFilter = m_cellFilterView()->currentTotalCellVisibility();
}
std::optional<std::vector<size_t>> timeSteps;
if ( !m_selectedTimeSteps().empty() )
{
timeSteps = std::vector<size_t>( m_selectedTimeSteps().begin(), m_selectedTimeSteps().end() );
}
bool evaluateDependentCalculations = true;
return calculateForCases( outputEclipseCases(), inputValueVisibilityFilter.p(), timeSteps, evaluateDependentCalculations );
return calculateForCases( { eclipseCase }, inputValueVisibilityFilter.p(), timeSteps, evaluateDependentCalculations );
}
//--------------------------------------------------------------------------------------------------
@@ -724,6 +782,30 @@ bool RimGridCalculation::allSourceCasesAreEqualToDestinationCase() const
return true;
}
//--------------------------------------------------------------------------------------------------
/// An aggregation expression (sum/avg/min/max/count) reduces a case's cell values to a single scalar
/// value per case. Unlike per-cell expressions, its result cannot be recomputed later for a single
/// realization on demand, so it must be computed for every realization up front.
//--------------------------------------------------------------------------------------------------
bool RimGridCalculation::isAggregationExpression() const
{
return m_expression().contains( "sum" ) || m_expression().contains( "avg" ) || m_expression().contains( "min" ) ||
m_expression().contains( "max" ) || m_expression().contains( "count" );
}
//--------------------------------------------------------------------------------------------------
/// For an ensemble destination with a plain per-cell expression, each realization is computed lazily
/// when its result is actually needed, so only the main case needs to be computed here. Aggregation
/// expressions produce the per-realization summary itself and must be computed for every realization.
//--------------------------------------------------------------------------------------------------
std::vector<RimEclipseCase*> RimGridCalculation::casesToCalculate() const
{
const bool calculateForEnsemble = m_destinationEnsemble() || m_additionalCasesType == AdditionalCasesType::ENSEMBLE;
if ( calculateForEnsemble && !isAggregationExpression() ) return { destinationCase() };
return outputEclipseCases();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -1025,9 +1107,7 @@ bool RimGridCalculation::calculateForCases( const std::vector<RimEclipseCase*>&
}
const bool isMultipleCasesPresent = calculationCases.size() > 1;
const bool hasAggregationExpression = m_expression().contains( "sum" ) || m_expression().contains( "avg" ) ||
m_expression().contains( "min" ) || m_expression().contains( "max" ) ||
m_expression().contains( "count" );
const bool hasAggregationExpression = isAggregationExpression();
// If multiple cases are present, release memory after data is extracted to avoid memory issues.
m_releaseMemoryAfterDataIsExtracted = isMultipleCasesPresent;
@@ -73,6 +73,7 @@ public:
bool preCalculate() const override;
bool calculate() override;
bool calculateForCase( RimEclipseCase* eclipseCase );
void updateDependentObjects() override;
void removeDependentObjects() override;
@@ -149,6 +150,9 @@ private:
bool allSourceCasesAreEqualToDestinationCase() const;
void onEditNonVisibleResultAddressButtonPressed();
bool isAggregationExpression() const;
std::vector<RimEclipseCase*> casesToCalculate() const;
static std::pair<bool, QStringList> createStatisticsText( const std::vector<std::vector<double>>& values );
private:
@@ -19,7 +19,9 @@
#include "RimGridCalculationCollection.h"
#include "RiaLogging.h"
#include "RigCaseCellResultsData.h"
#include "RigEclipseResultAddress.h"
#include "RimEclipseCase.h"
#include "RimGridCalculation.h"
#include "cafPdmUiGroup.h"
@@ -132,6 +134,27 @@ RimGridCalculation* RimGridCalculationCollection::findCalculation( const QString
return nullptr;
}
//--------------------------------------------------------------------------------------------------
/// Grid calculation results are not stored with the case. Compute the calculations missing or
/// invalidated (cleared, but still registered) for the case, e.g. when a view is stepped to another
/// case.
//--------------------------------------------------------------------------------------------------
void RimGridCalculationCollection::ensureCalculationsAreComputed( RimEclipseCase* eclipseCase ) const
{
if ( !eclipseCase || !eclipseCase->ensureReservoirCaseIsOpen() ) return;
auto results = eclipseCase->results( RiaDefines::PorosityModelType::MATRIX_MODEL );
if ( !results ) return;
for ( auto calculation : sortedGridCalculations() )
{
const RigEclipseResultAddress address = calculation->outputAddress();
if ( results->hasResultEntry( address ) && results->isResultLoaded( address ) ) continue;
calculation->calculateForCase( eclipseCase );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -37,9 +37,10 @@ public:
RimGridCalculation* createCalculation() const override;
std::vector<RimGridCalculation*> sortedGridCalculations() const;
std::vector<RimGridCalculation*> dependentCalculations( RimGridCalculation* sourceCalculation ) const;
RimGridCalculation* findCalculation( const QString& calculationName ) const;
void ensureCalculationsAreComputed( RimEclipseCase* eclipseCase ) const;
private:
RimGridCalculation* findCalculation( const QString& calculationName ) const;
bool dependentCalculationsRecursively( RimGridCalculation* sourceCalculation, std::vector<RimGridCalculation*>& calculations ) const;
void initAfterRead() override;