From b7803ee320e49e55874bf17fa9b9eb6ff54daa59 Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Wed, 15 Jul 2026 11:03:31 +0200 Subject: [PATCH 01/12] #14345 Grid Calculation: replace undefined values with default value in aggregations Aggregation functions like sum() in the expression parser include all values in the input vector. Undefined values (HUGE_VAL) could leak into the aggregation input when a grid case group member has fewer time steps on disk than the main case it inherits result meta data from, or when a cell is active in the calculation case but has no value in the source data. This produced infinite sums for some realizations, while the histogram statistics ignore undefined values. Replace undefined values in the aggregation input with the default value 0.0, matching the histogram semantics, and log a warning per case and variable when values were replaced. --- .../ProjectDataModel/RimGridCalculation.cpp | 51 +++++++++++++-- .../ProjectDataModel/RimGridCalculation.h | 2 + ApplicationLibCode/UnitTests/CMakeLists.txt | 1 + .../UnitTests/RimGridCalculation-Test.cpp | 62 +++++++++++++++++++ 4 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 ApplicationLibCode/UnitTests/RimGridCalculation-Test.cpp diff --git a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp index 4ba2d3810c8..7c840cd0765 100644 --- a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp @@ -35,6 +35,7 @@ #include "RigResultAccessor.h" #include "RigResultAccessorFactory.h" #include "RigStatisticsMath.h" +#include "RigStatisticsTools.h" #include "RimCaseCollection.h" #include "RimEclipseCase.h" @@ -704,6 +705,27 @@ void RimGridCalculation::replaceFilteredValuesWithDefaultValue( double } } +//-------------------------------------------------------------------------------------------------- +/// Replace undefined values (infinity and NaN) with the given default value. Returns the number of +/// replaced values. +//-------------------------------------------------------------------------------------------------- +size_t RimGridCalculation::replaceInvalidValuesWithDefaultValue( double defaultValue, std::vector& values ) +{ + size_t replacedCount = 0; + +#pragma omp parallel for reduction( + : replacedCount ) + for ( int i = 0; i < static_cast( values.size() ); i++ ) + { + if ( RigStatisticsTools::isInvalidNumber( values[i] ) ) + { + values[i] = defaultValue; + replacedCount++; + } + } + + return replacedCount; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -898,6 +920,7 @@ bool RimGridCalculation::calculateForCases( const std::vector& scalarResultFrames->resize( timeStepCount ); std::vector aggregatedValuesOneTimeStep; + std::vector invalidValueCountPerVariable( m_variables.size(), 0 ); for ( size_t tsId = 0; tsId < timeStepCount; tsId++ ) { @@ -919,11 +942,19 @@ bool RimGridCalculation::calculateForCases( const std::vector& { RiaLogging::error( std::format( " No data found for variable '{}'.", v->name() ) ); } - else if ( inputValueVisibilityFilter && hasAggregationExpression ) + else if ( hasAggregationExpression ) { - const double defaultValue = 0.0; - auto activeCellInfo = calculationCase->eclipseCaseData()->activeCellInfo( porosityModel ); - replaceFilteredValuesWithDefaultValue( defaultValue, inputValueVisibilityFilter, dataForVariable, activeCellInfo ); + const double defaultValue = 0.0; + if ( inputValueVisibilityFilter ) + { + auto activeCellInfo = calculationCase->eclipseCaseData()->activeCellInfo( porosityModel ); + replaceFilteredValuesWithDefaultValue( defaultValue, inputValueVisibilityFilter, dataForVariable, activeCellInfo ); + } + + // Aggregation functions include all values in the vector. Replace undefined values with the + // default value to avoid contaminating aggregated values like sum() with infinity. Undefined + // values are ignored by other statistics computations, like the histogram in the 3d view. + invalidValueCountPerVariable[i] += replaceInvalidValuesWithDefaultValue( defaultValue, dataForVariable ); } dataForAllVariables.push_back( dataForVariable ); @@ -995,6 +1026,18 @@ bool RimGridCalculation::calculateForCases( const std::vector& calculationCase->updateResultAddressCollection(); } + for ( size_t i = 0; i < m_variables.size(); i++ ) + { + if ( invalidValueCountPerVariable[i] > 0 ) + { + RiaLogging::warning( std::format( " Variable '{}': {} undefined input values were replaced with 0.0 for case '{}'. The " + "input result may be missing data for some cells or time steps.", + m_variables[i]->name().toStdString(), + invalidValueCountPerVariable[i], + calculationCase->caseUserDescription().toStdString() ) ); + } + } + if ( hasAggregationExpression ) { QString txt = " " + calculationCase->caseUserDescription(); diff --git a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h index fc162ceeb64..ada563ec4e5 100644 --- a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h +++ b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h @@ -86,6 +86,8 @@ class RimGridCalculation : public RimUserDefinedCalculation RimGridCalculationVariable* createVariable() override; + static size_t replaceInvalidValuesWithDefaultValue( double defaultValue, std::vector& values ); + protected: void onChildrenUpdated( caf::PdmChildArrayFieldHandle* childArray, std::vector& updatedObjects ) override; diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index 6a9e74ea075..92e0d993965 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -129,6 +129,7 @@ set(SOURCE_UNITTEST_FILES ${CMAKE_CURRENT_LIST_DIR}/opm-import-well-data-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifInpExportTools-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifGridCalculationIO-Test.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimGridCalculation-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifSummaryCalculationIO-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RimEmReader-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RimFormationNames-Test.cpp diff --git a/ApplicationLibCode/UnitTests/RimGridCalculation-Test.cpp b/ApplicationLibCode/UnitTests/RimGridCalculation-Test.cpp new file mode 100644 index 00000000000..db65d7e9840 --- /dev/null +++ b/ApplicationLibCode/UnitTests/RimGridCalculation-Test.cpp @@ -0,0 +1,62 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RimGridCalculation.h" + +#include +#include + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RimGridCalculationTest, ReplaceInvalidValuesWithDefaultValue ) +{ + const double defaultValue = 0.0; + std::vector values = + { 1.0, HUGE_VAL, 2.0, -HUGE_VAL, std::numeric_limits::quiet_NaN(), std::numeric_limits::infinity(), -3.0, 0.0 }; + + size_t replacedCount = RimGridCalculation::replaceInvalidValuesWithDefaultValue( defaultValue, values ); + + EXPECT_EQ( 4u, replacedCount ); + + const std::vector expected = { 1.0, 0.0, 2.0, 0.0, 0.0, 0.0, -3.0, 0.0 }; + ASSERT_EQ( expected.size(), values.size() ); + for ( size_t i = 0; i < expected.size(); i++ ) + { + EXPECT_DOUBLE_EQ( expected[i], values[i] ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RimGridCalculationTest, ReplaceInvalidValuesWithDefaultValueNoInvalidValues ) +{ + const std::vector original = { 1.0, 2.0, -3.0, 0.0 }; + + std::vector values = original; + size_t replacedCount = RimGridCalculation::replaceInvalidValuesWithDefaultValue( 42.0, values ); + + EXPECT_EQ( 0u, replacedCount ); + EXPECT_EQ( original, values ); + + std::vector emptyValues; + EXPECT_EQ( 0u, RimGridCalculation::replaceInvalidValuesWithDefaultValue( 42.0, emptyValues ) ); +} From 64e149997f7b8412e3278a687c30d4e92f6cf3a8 Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Wed, 15 Jul 2026 11:51:50 +0200 Subject: [PATCH 02/12] #14345 Add debug logging of value counts used in aggregations and histogram statistics Log at debug level, for each case and time step, how many input values contribute to a grid calculation aggregation and how many were replaced with the default value (non-visible cells and undefined values). Also log the sum and number of values used when computing visible cells statistics, to allow comparison with the histogram sum in the 3d view. --- .../ProjectDataModel/RimGridCalculation.cpp | 37 ++++++++++++++----- .../ProjectDataModel/RimGridCalculation.h | 8 ++-- .../RigEclipseNativeVisibleCellsStatCalc.cpp | 8 ++++ 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp index 7c840cd0765..86f85cc7954 100644 --- a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp @@ -681,12 +681,13 @@ void RimGridCalculation::replaceFilteredValuesWithVector( const std::vector visibility, - std::vector& resultValues, - RigActiveCellInfo* activeCellInfo ) +size_t RimGridCalculation::replaceFilteredValuesWithDefaultValue( double defaultValue, + cvf::ref visibility, + std::vector& resultValues, + RigActiveCellInfo* activeCellInfo ) { auto activeReservoirCellIndices = activeCellInfo->activeReservoirCellIndices(); @@ -694,15 +695,20 @@ void RimGridCalculation::replaceFilteredValuesWithDefaultValue( double CAF_ASSERT( numActiveCells == (int)resultValues.size() ); -#pragma omp parallel for + size_t replacedCount = 0; + +#pragma omp parallel for reduction( + : replacedCount ) for ( int i = 0; i < numActiveCells; i++ ) { const auto reservoirCellIndex = activeReservoirCellIndices[i]; if ( !visibility->val( reservoirCellIndex.value() ) ) { resultValues[i] = defaultValue; + replacedCount++; } } + + return replacedCount; } //-------------------------------------------------------------------------------------------------- @@ -944,17 +950,30 @@ bool RimGridCalculation::calculateForCases( const std::vector& } else if ( hasAggregationExpression ) { - const double defaultValue = 0.0; + const double defaultValue = 0.0; + size_t nonVisibleCount = 0; if ( inputValueVisibilityFilter ) { auto activeCellInfo = calculationCase->eclipseCaseData()->activeCellInfo( porosityModel ); - replaceFilteredValuesWithDefaultValue( defaultValue, inputValueVisibilityFilter, dataForVariable, activeCellInfo ); + nonVisibleCount = + replaceFilteredValuesWithDefaultValue( defaultValue, inputValueVisibilityFilter, dataForVariable, activeCellInfo ); } // Aggregation functions include all values in the vector. Replace undefined values with the // default value to avoid contaminating aggregated values like sum() with infinity. Undefined // values are ignored by other statistics computations, like the histogram in the 3d view. - invalidValueCountPerVariable[i] += replaceInvalidValuesWithDefaultValue( defaultValue, dataForVariable ); + const size_t invalidCount = replaceInvalidValuesWithDefaultValue( defaultValue, dataForVariable ); + invalidValueCountPerVariable[i] += invalidCount; + + RiaLogging::debug( std::format( " Case '{}', time step {}: variable '{}': {} of {} values used in aggregation " + "({} non-visible and {} undefined values replaced with 0.0)", + calculationCase->caseUserDescription().toStdString(), + tsId, + v->name().toStdString(), + dataForVariable.size() - nonVisibleCount - invalidCount, + dataForVariable.size(), + nonVisibleCount, + invalidCount ) ); } dataForAllVariables.push_back( dataForVariable ); diff --git a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h index ada563ec4e5..a16680c2aca 100644 --- a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h +++ b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h @@ -120,10 +120,10 @@ class RimGridCalculation : public RimUserDefinedCalculation std::vector& resultValues, RigActiveCellInfo* activeCellInfo ); - static void replaceFilteredValuesWithDefaultValue( double defaultValue, - cvf::ref visibility, - std::vector& resultValues, - RigActiveCellInfo* activeCellInfo ); + static size_t replaceFilteredValuesWithDefaultValue( double defaultValue, + cvf::ref visibility, + std::vector& resultValues, + RigActiveCellInfo* activeCellInfo ); using DefaultValueConfig = std::pair; DefaultValueConfig defaultValueConfiguration() const; diff --git a/ApplicationLibCode/ReservoirDataModel/RigEclipseNativeVisibleCellsStatCalc.cpp b/ApplicationLibCode/ReservoirDataModel/RigEclipseNativeVisibleCellsStatCalc.cpp index 6d7ee8aa875..94a515a6b71 100644 --- a/ApplicationLibCode/ReservoirDataModel/RigEclipseNativeVisibleCellsStatCalc.cpp +++ b/ApplicationLibCode/ReservoirDataModel/RigEclipseNativeVisibleCellsStatCalc.cpp @@ -19,6 +19,7 @@ #include "RigEclipseNativeVisibleCellsStatCalc.h" +#include "RiaLogging.h" #include "RiaResultNames.h" #include "RigActiveCellInfo.h" @@ -27,6 +28,7 @@ #include "RigWeightedMeanCalc.h" #include +#include //-------------------------------------------------------------------------------------------------- /// @@ -104,6 +106,12 @@ void RigEclipseNativeVisibleCellsStatCalc::valueSumAndSampleCount( size_t timeSt traverseCells( acc, timeStepIndex ); valueSum = acc.valueSum; sampleCount = acc.sampleCount; + + RiaLogging::debug( std::format( "Visible cells statistics for '{}', time step {}: sum = {}, number of values used = {}", + m_resultAddress.resultName().toStdString(), + timeStepIndex, + valueSum, + sampleCount ) ); } //-------------------------------------------------------------------------------------------------- From d6893ab94625a5c54843cfe9e2f981a98ac5914c Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Wed, 15 Jul 2026 12:22:17 +0200 Subject: [PATCH 03/12] #14345 Compute ResInsight-calculated results when loading a single time step findOrLoadKnownScalarResultForTimeStep() tried to read ResInsight- calculated results like riOILVOLUME from the restart file. For lazily loaded grid case group members these results are never computed, so every cell ended up undefined and the grid calculator produced zero sums for all additional cases. Delegate to findOrLoadKnownScalarResult(), which dispatches to the result calculators, for results flagged as must-be-calculated and for the ri* volume results. Already computed data is not computed again. --- .../ReservoirDataModel/RigCaseCellResultsData.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.cpp b/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.cpp index 5f3a664120b..1220d22e5ec 100644 --- a/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.cpp +++ b/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.cpp @@ -1762,6 +1762,17 @@ size_t RigCaseCellResultsData::findOrLoadKnownScalarResultForTimeStep( const Rig if ( scalarResultIndex == cvf::UNDEFINED_SIZE_T ) return cvf::UNDEFINED_SIZE_T; if ( type == RiaDefines::ResultCatType::GENERATED ) return scalarResultIndex; + // Results computed by ResInsight (e.g. riOILVOLUME) can not be read from file for a single time step. + // Delegate to findOrLoadKnownScalarResult(), which computes all time steps using the result + // calculators. Data already present is not computed again. + const bool isComputedResult = mustBeCalculated( scalarResultIndex ) || resultName == RiaResultNames::riCellVolumeResultName() || + resultName == RiaResultNames::riOilVolumeResultName() || resultName == RiaResultNames::riPorvSoil() || + resultName == RiaResultNames::riPorvSgas() || resultName == RiaResultNames::riPorvSoilSgas(); + if ( isComputedResult ) + { + return findOrLoadKnownScalarResult( resVarAddr ); + } + if ( m_readerInterface.notNull() ) { size_t timeStepCount = infoForEachResultIndex()[scalarResultIndex].timeStepInfos().size(); From 0f513625ee906dc274f4d00b4193fb68f92b2c05 Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Wed, 15 Jul 2026 12:59:12 +0200 Subject: [PATCH 04/12] #14345 Recompute calculated results when data for a time step has been released The grid calculator releases result data per time step to reduce memory usage when calculating for many cases. If only some time steps are released, findOrLoadKnownScalarResult() returned early because data was present for other time steps, leaving the released time steps empty. Results computed by ResInsight, like riOILVOLUME, then appeared as undefined in the 3d view after running a calculation for a selected time step. Recompute results computed by ResInsight when data is missing for any time step. Skip the delegation from the single time step variant when the requested time step already has data, to avoid repeated recomputation while the calculator releases other time steps. --- .../RigCaseCellResultsData.cpp | 46 ++++++++++++++++--- .../RigCaseCellResultsData.h | 3 ++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.cpp b/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.cpp index 1220d22e5ec..c3266618bd3 100644 --- a/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.cpp +++ b/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.cpp @@ -1504,7 +1504,15 @@ size_t RigCaseCellResultsData::findOrLoadKnownScalarResult( const RigEclipseResu if ( isDataPresent( scalarResultIndex ) ) { - return scalarResultIndex; + // Data for some time steps can have been released to reduce memory usage, see + // RimGridCalculation::getActiveCellValues(). Results computed by ResInsight can not be reloaded from + // file, so continue to recompute them if data is missing for any time step. + const bool mustRecompute = isResultComputedByResInsight( resultName, scalarResultIndex ) && + hasEmptyDataForAnyTimeStep( scalarResultIndex ); + if ( !mustRecompute ) + { + return scalarResultIndex; + } } if ( resultName == RiaResultNames::soil() ) @@ -1764,12 +1772,16 @@ size_t RigCaseCellResultsData::findOrLoadKnownScalarResultForTimeStep( const Rig // Results computed by ResInsight (e.g. riOILVOLUME) can not be read from file for a single time step. // Delegate to findOrLoadKnownScalarResult(), which computes all time steps using the result - // calculators. Data already present is not computed again. - const bool isComputedResult = mustBeCalculated( scalarResultIndex ) || resultName == RiaResultNames::riCellVolumeResultName() || - resultName == RiaResultNames::riOilVolumeResultName() || resultName == RiaResultNames::riPorvSoil() || - resultName == RiaResultNames::riPorvSgas() || resultName == RiaResultNames::riPorvSoilSgas(); - if ( isComputedResult ) + // calculators. Skip the delegation if data is already present for the requested time step, to avoid + // repeated recomputation when data for other time steps is released during a calculation. + if ( isResultComputedByResInsight( resultName, scalarResultIndex ) ) { + const auto& valuesAllTimeSteps = m_cellScalarResults[scalarResultIndex]; + if ( timeStepIndex < valuesAllTimeSteps.size() && !valuesAllTimeSteps[timeStepIndex].empty() ) + { + return scalarResultIndex; + } + return findOrLoadKnownScalarResult( resVarAddr ); } @@ -3154,6 +3166,28 @@ bool RigCaseCellResultsData::isDataPresent( size_t scalarResultIndex ) const return allocatedValueCount( scalarResultIndex ) > 0; } +//-------------------------------------------------------------------------------------------------- +/// Returns true for results that are computed by ResInsight and can not be read from file +//-------------------------------------------------------------------------------------------------- +bool RigCaseCellResultsData::isResultComputedByResInsight( const QString& resultName, size_t scalarResultIndex ) const +{ + return mustBeCalculated( scalarResultIndex ) || resultName == RiaResultNames::riCellVolumeResultName() || + resultName == RiaResultNames::riOilVolumeResultName() || resultName == RiaResultNames::riPorvSoil() || + resultName == RiaResultNames::riPorvSgas() || resultName == RiaResultNames::riPorvSoilSgas(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RigCaseCellResultsData::hasEmptyDataForAnyTimeStep( size_t scalarResultIndex ) const +{ + if ( scalarResultIndex >= resultCount() ) return false; + + const std::vector>& valuesAllTimeSteps = m_cellScalarResults[scalarResultIndex]; + + return std::any_of( valuesAllTimeSteps.begin(), valuesAllTimeSteps.end(), []( const auto& values ) { return values.empty(); } ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.h b/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.h index 2108510267e..c181cefaab8 100644 --- a/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.h +++ b/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.h @@ -215,6 +215,9 @@ class RigCaseCellResultsData : public cvf::Object bool mustBeCalculated( size_t scalarResultIndex ) const; void setMustBeCalculated( size_t scalarResultIndex ); + bool isResultComputedByResInsight( const QString& resultName, size_t scalarResultIndex ) const; + bool hasEmptyDataForAnyTimeStep( size_t scalarResultIndex ) const; + void computeSOILForTimeStep( size_t timeStepIndex ); void testAndComputeSgasForTimeStep( size_t timeStepIndex ); From 13d21654075048b33f72c31fa4af7e5c6ca40e8e Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Wed, 15 Jul 2026 13:21:04 +0200 Subject: [PATCH 05/12] #14345 Use cell filter geometry for grid calculation visibility The visibility mask for 'filter by view' was taken from the view's visible cells, which are limited to the active cells of the view's case. When applying the calculation to additional cases with different ACTNUM, cells inside the filter geometry that are inactive in the view's case were excluded from the aggregation for cases where they are active, giving too low sums. Build the mask from the cell filter geometry (RANGE_FILTERED + RANGE_FILTERED_INACTIVE) instead, so each calculation case contributes its own active cells inside the filters. Property filters in the view no longer affect the calculation. The result filtering now uses the same mask as the input filtering, also when the visibility filter is provided by the caller. --- .../ProjectDataModel/RimGridCalculation.cpp | 27 +++++++++++++------ .../ProjectDataModel/RimGridCalculation.h | 2 +- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp index 86f85cc7954..518f3094e16 100644 --- a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp @@ -243,10 +243,23 @@ bool RimGridCalculation::calculate() } } - cvf::UByteArray* inputValueVisibilityFilter = nullptr; + cvf::ref inputValueVisibilityFilter; if ( m_cellFilterView() ) { - inputValueVisibilityFilter = m_cellFilterView()->currentTotalCellVisibility().p(); + if ( auto eclipseView = dynamic_cast( m_cellFilterView() ) ) + { + // Use the cell filter geometry, independent of the active cells in the view's case. Cells inside the + // filters that are inactive in the view's case can be active in other calculation cases, and must be + // included when the calculation is applied to additional cases. + inputValueVisibilityFilter = new cvf::UByteArray; + eclipseView->calculateCellVisibility( inputValueVisibilityFilter.p(), + { RANGE_FILTERED, RANGE_FILTERED_INACTIVE }, + eclipseView->currentTimeStep() ); + } + else + { + inputValueVisibilityFilter = m_cellFilterView()->currentTotalCellVisibility(); + } } std::optional> timeSteps = std::nullopt; @@ -263,7 +276,7 @@ bool RimGridCalculation::calculate() } bool evaluateDependentCalculations = true; - return calculateForCases( outputEclipseCases(), inputValueVisibilityFilter, timeSteps, evaluateDependentCalculations ); + return calculateForCases( outputEclipseCases(), inputValueVisibilityFilter.p(), timeSteps, evaluateDependentCalculations ); } //-------------------------------------------------------------------------------------------------- @@ -735,7 +748,7 @@ size_t RimGridCalculation::replaceInvalidValuesWithDefaultValue( double defaultV //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -void RimGridCalculation::filterResults( RimGridView* cellFilterView, +void RimGridCalculation::filterResults( cvf::UByteArray* visibility, const std::vector>& values, size_t timeStep, RimGridCalculation::DefaultValueType defaultValueType, @@ -745,8 +758,6 @@ void RimGridCalculation::filterResults( RimGridView* RimEclipseCase* outputEclipseCase ) const { - auto visibility = cellFilterView->currentTotalCellVisibility(); - auto activeCellInfo = outputEclipseCase->eclipseCaseData()->activeCellInfo( porosityModel ); if ( defaultValueType == RimGridCalculation::DefaultValueType::FROM_PROPERTY ) @@ -1017,9 +1028,9 @@ bool RimGridCalculation::calculateForCases( const std::vector& } } - if ( m_cellFilterView() && !resultValues.empty() ) + if ( inputValueVisibilityFilter && !resultValues.empty() ) { - filterResults( m_cellFilterView(), + filterResults( inputValueVisibilityFilter, dataForAllVariables, tsId, m_defaultValueType(), diff --git a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h index a16680c2aca..1a3a127e422 100644 --- a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h +++ b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h @@ -106,7 +106,7 @@ class RimGridCalculation : public RimUserDefinedCalculation RimEclipseCase* sourceCase, RimEclipseCase* destinationCase ) const; - void filterResults( RimGridView* cellFilterView, + void filterResults( cvf::UByteArray* visibility, const std::vector>& values, size_t timeStep, RimGridCalculation::DefaultValueType defaultValueType, From b62540eda0a4e52c785fe632ca007ffb2fa939af Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Wed, 15 Jul 2026 15:01:05 +0200 Subject: [PATCH 06/12] #14345 Add per-case cell filter evaluation with source case override Add a sourceCaseOverride parameter to RimCellFilter::applyToCellVisibility. When set, property filters evaluate against the override case's result values instead of the case the filter is bound to, allowing the same filter definition to be evaluated against each case in an ensemble. Combined filters forward the override to their children, and geometry based filters ignore it. Add RimCellFilterTools::computeReservoirCellVisibility, which evaluates a cell filter against a given case for all grids and returns the visibility indexed by reservoir cell index. Range filters propagate the parent grid visibility into LGR cells, matching the filtered geometry of a 3d view. Also add RimCellFilter::setFilterMode. --- .../CellFilters/CMakeLists_files.cmake | 1 + .../CellFilters/RimCellFilter.cpp | 13 +- .../CellFilters/RimCellFilter.h | 9 +- .../CellFilters/RimCellFilterTools.cpp | 103 ++++++++++ .../CellFilters/RimCellFilterTools.h | 34 ++++ .../CellFilters/RimCombinedFilter.cpp | 7 +- .../CellFilters/RimCombinedFilter.h | 5 +- .../CellFilters/RimEclipsePropertyFilter.cpp | 35 +++- .../CellFilters/RimEclipsePropertyFilter.h | 5 +- ApplicationLibCode/UnitTests/CMakeLists.txt | 1 + .../UnitTests/RimCellFilterTools-Test.cpp | 180 ++++++++++++++++++ 11 files changed, 377 insertions(+), 16 deletions(-) create mode 100644 ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilterTools.cpp create mode 100644 ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilterTools.h create mode 100644 ApplicationLibCode/UnitTests/RimCellFilterTools-Test.cpp diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/CMakeLists_files.cmake b/ApplicationLibCode/ProjectDataModel/CellFilters/CMakeLists_files.cmake index 41dd8e82fcb..288b829048c 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/CMakeLists_files.cmake +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/CMakeLists_files.cmake @@ -1,6 +1,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RimCellFilter.cpp ${CMAKE_CURRENT_LIST_DIR}/RimCellFilterCollection.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimCellFilterTools.cpp ${CMAKE_CURRENT_LIST_DIR}/RimCellRangeFilter.cpp ${CMAKE_CURRENT_LIST_DIR}/RimCombinedFilter.cpp ${CMAKE_CURRENT_LIST_DIR}/RimDataFilterCollection.cpp diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilter.cpp b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilter.cpp index 37ad312a4f8..534d3f4f813 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilter.cpp +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilter.cpp @@ -199,6 +199,14 @@ caf::AppEnum RimCellFilter::filterMode() const return m_filterMode(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCellFilter::setFilterMode( FilterModeType filterMode ) +{ + m_filterMode = filterMode; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -322,7 +330,10 @@ QList RimCellFilter::calculateValueOptions( const caf::P //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -void RimCellFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility, const RigGridBase* grid, size_t /*timeStepIndex*/ ) +void RimCellFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility, + const RigGridBase* grid, + size_t /*timeStepIndex*/, + RimEclipseCase* /*sourceCaseOverride*/ ) { if ( cellVisibility == nullptr || grid == nullptr ) return; diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilter.h b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilter.h index dfcab766b02..14ae3bd7ec2 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilter.h +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilter.h @@ -79,6 +79,7 @@ class RimCellFilter : public RimCheckableNamedObject virtual bool isFilterEnabled() const; caf::AppEnum filterMode() const; + void setFilterMode( FilterModeType filterMode ); QString modeString() const; bool propagateToSubGrids() const; @@ -97,7 +98,13 @@ class RimCellFilter : public RimCheckableNamedObject // Unified evaluation: take an incoming per-cell visibility mask and hide the cells that // this filter rejects (respecting its own INCLUDE/EXCLUDE mode). The default implementation // bridges the legacy range/index dispatch so existing subclasses do not need to override. - virtual void applyToCellVisibility( cvf::UByteArray* cellVisibility, const RigGridBase* grid, size_t timeStepIndex ); + // When sourceCaseOverride is set, property filters evaluate against that case's results instead + // of the case the filter is bound to, allowing the same filter definition to be evaluated + // against each case in an ensemble. + virtual void applyToCellVisibility( cvf::UByteArray* cellVisibility, + const RigGridBase* grid, + size_t timeStepIndex, + RimEclipseCase* sourceCaseOverride = nullptr ); protected: caf::PdmFieldHandle* userDescriptionField() override; diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilterTools.cpp b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilterTools.cpp new file mode 100644 index 00000000000..10ae5d706f4 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilterTools.cpp @@ -0,0 +1,103 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RimCellFilterTools.h" + +#include "RigEclipseCaseData.h" +#include "RigGridBase.h" +#include "RigLocalGrid.h" +#include "RigMainGrid.h" + +#include "RimCellFilter.h" +#include "RimEclipseCase.h" + +//-------------------------------------------------------------------------------------------------- +/// Evaluate the filter against the given case for all grids, and return the visibility indexed by +/// reservoir cell index. Property filters are evaluated against the given case's own result values. +//-------------------------------------------------------------------------------------------------- +cvf::ref + RimCellFilterTools::computeReservoirCellVisibility( RimCellFilter* filter, RimEclipseCase* eclipseCase, size_t timeStepIndex ) +{ + if ( !filter || !eclipseCase || !eclipseCase->eclipseCaseData() || !eclipseCase->eclipseCaseData()->mainGrid() ) return nullptr; + + RigEclipseCaseData* caseData = eclipseCase->eclipseCaseData(); + + cvf::ref reservoirVisibility = new cvf::UByteArray( caseData->mainGrid()->totalCellCount() ); + reservoirVisibility->setAll( false ); + + const bool isInclude = ( filter->filterMode() == RimCellFilter::INCLUDE ); + + // Grid local masks are kept to allow propagation of parent grid visibility into LGRs + std::vector> gridMasks( caseData->gridCount() ); + + for ( size_t gridIndex = 0; gridIndex < caseData->gridCount(); gridIndex++ ) + { + RigGridBase* grid = caseData->grid( gridIndex ); + + gridMasks[gridIndex] = new cvf::UByteArray( grid->cellCount() ); + cvf::UByteArray& gridMask = *gridMasks[gridIndex]; + gridMask.setAll( true ); + + if ( filter->isRangeFilter() ) + { + // Range filters evaluate only on their target grid. On other grids an INCLUDE filter + // contributes no cells, while an EXCLUDE filter removes none. Cells in LGRs follow the + // visibility of their parent grid cell, as in the filtered geometry of a 3d view. + const bool isTargetGrid = ( filter->gridIndex() == static_cast( gridIndex ) ); + if ( isTargetGrid ) + { + filter->applyToCellVisibility( &gridMask, grid, timeStepIndex, eclipseCase ); + } + else + { + gridMask.setAll( !isInclude ); + } + + if ( !grid->isMainGrid() ) + { + auto localGrid = static_cast( grid ); + const cvf::UByteArray& parentMask = *gridMasks[localGrid->parentGrid()->gridIndex()]; + + for ( size_t localIdx = 0; localIdx < grid->cellCount(); localIdx++ ) + { + const size_t parentCellIndex = grid->cell( localIdx ).parentCellIndex(); + if ( isInclude ) + { + gridMask[localIdx] = gridMask[localIdx] || parentMask[parentCellIndex]; + } + else + { + gridMask[localIdx] = gridMask[localIdx] && parentMask[parentCellIndex]; + } + } + } + } + else + { + // Index filters (polygon, user defined) and property filters evaluate on all grids + filter->applyToCellVisibility( &gridMask, grid, timeStepIndex, eclipseCase ); + } + + for ( size_t localIdx = 0; localIdx < grid->cellCount(); localIdx++ ) + { + reservoirVisibility->set( grid->reservoirCellIndex( localIdx ), gridMask[localIdx] ); + } + } + + return reservoirVisibility; +} diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilterTools.h b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilterTools.h new file mode 100644 index 00000000000..56849d2ee62 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilterTools.h @@ -0,0 +1,34 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "cvfArray.h" +#include "cvfObject.h" + +class RimCellFilter; +class RimEclipseCase; + +//================================================================================================== +/// +//================================================================================================== +class RimCellFilterTools +{ +public: + static cvf::ref computeReservoirCellVisibility( RimCellFilter* filter, RimEclipseCase* eclipseCase, size_t timeStepIndex ); +}; diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCombinedFilter.cpp b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCombinedFilter.cpp index a1952faef09..8f6ffc3ac06 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCombinedFilter.cpp +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCombinedFilter.cpp @@ -104,7 +104,10 @@ void RimCombinedFilter::onGridChanged() /// INCLUDE/EXCLUDE mode inside applyToCellVisibility), AND/OR combine the masks, then apply this /// combined filter's INCLUDE/EXCLUDE mode onto the incoming cellVisibility. //-------------------------------------------------------------------------------------------------- -void RimCombinedFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility, const RigGridBase* grid, size_t timeStepIndex ) +void RimCombinedFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility, + const RigGridBase* grid, + size_t timeStepIndex, + RimEclipseCase* sourceCaseOverride ) { if ( cellVisibility == nullptr || grid == nullptr ) return; @@ -125,7 +128,7 @@ void RimCombinedFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility, { cvf::UByteArray childMask( n ); childMask.setAll( 1 ); - child->applyToCellVisibility( &childMask, grid, timeStepIndex ); + child->applyToCellVisibility( &childMask, grid, timeStepIndex, sourceCaseOverride ); if ( m_combineMode() == CombineMode::AND ) { diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCombinedFilter.h b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCombinedFilter.h index 20f0fc35308..964c37335ce 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCombinedFilter.h +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCombinedFilter.h @@ -49,7 +49,10 @@ class RimCombinedFilter : public RimCellFilter bool isFilterEnabled() const override; void onGridChanged() override; - void applyToCellVisibility( cvf::UByteArray* cellVisibility, const RigGridBase* grid, size_t timeStepIndex ) override; + void applyToCellVisibility( cvf::UByteArray* cellVisibility, + const RigGridBase* grid, + size_t timeStepIndex, + RimEclipseCase* sourceCaseOverride = nullptr ) override; // Bridge from legacy collection dispatch: combined filter is declared INDEX-type, so the cell // filter collection's evaluation path (if ever called) would route through updateCellIndexFilter. diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.cpp b/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.cpp index eb5181e188a..097e2ed6fe4 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.cpp +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.cpp @@ -724,21 +724,36 @@ void RimEclipsePropertyFilter::initAfterRead() /// uniformly; the body is extracted from the former inline loop in /// RivReservoirViewPartMgr::computePropertyVisibility. //-------------------------------------------------------------------------------------------------- -void RimEclipsePropertyFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility, const RigGridBase* grid, size_t timeStepIndex ) +void RimEclipsePropertyFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility, + const RigGridBase* grid, + size_t timeStepIndex, + RimEclipseCase* sourceCaseOverride ) { if ( cellVisibility == nullptr || grid == nullptr ) return; if ( !isActive() || !resultDefinition()->hasResult() ) return; - resultDefinition()->loadResult(); - - // The result definition carries its own eclipse case binding (set via setEclipseCase). Prefer - // that, since case-level data filters have no view-side property-filter-collection ancestor. - RimEclipseCase* ec = resultDefinition()->eclipseCase(); - if ( !ec ) + RimEclipseCase* ec = sourceCaseOverride; + if ( ec ) { - auto* container = parentContainer(); - auto* view = container ? container->reservoirView() : nullptr; - ec = view ? view->eclipseCase() : nullptr; + // Evaluate against the override case's results. Make sure the result is loaded for that case. + if ( auto cellResultsData = ec->results( resultDefinition()->porosityModel() ) ) + { + cellResultsData->ensureKnownResultLoaded( resultDefinition()->eclipseResultAddress() ); + } + } + else + { + resultDefinition()->loadResult(); + + // The result definition carries its own eclipse case binding (set via setEclipseCase). Prefer + // that, since case-level data filters have no view-side property-filter-collection ancestor. + ec = resultDefinition()->eclipseCase(); + if ( !ec ) + { + auto* container = parentContainer(); + auto* view = container ? container->reservoirView() : nullptr; + ec = view ? view->eclipseCase() : nullptr; + } } if ( !ec ) return; RigEclipseCaseData* eclipseCase = ec->eclipseCaseData(); diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.h b/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.h index fb900cb3fe0..ed04eb15a7d 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.h +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.h @@ -59,7 +59,10 @@ class RimEclipsePropertyFilter : public RimPropertyFilter, public RimFieldQuickA void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override; void initAfterRead() override; - void applyToCellVisibility( cvf::UByteArray* cellVisibility, const RigGridBase* grid, size_t timeStepIndex ) override; + void applyToCellVisibility( cvf::UByteArray* cellVisibility, + const RigGridBase* grid, + size_t timeStepIndex, + RimEclipseCase* sourceCaseOverride = nullptr ) override; void updateUiFieldsFromActiveResult(); diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index 92e0d993965..64a20c33ea9 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -129,6 +129,7 @@ set(SOURCE_UNITTEST_FILES ${CMAKE_CURRENT_LIST_DIR}/opm-import-well-data-Test.cpp ${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}/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 new file mode 100644 index 00000000000..597f78247e1 --- /dev/null +++ b/ApplicationLibCode/UnitTests/RimCellFilterTools-Test.cpp @@ -0,0 +1,180 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RiaResultNames.h" +#include "RiaTestDataDirectory.h" + +#include "RigActiveCellInfo.h" +#include "RigCaseCellResultsData.h" +#include "RigEclipseCaseData.h" +#include "RigEclipseResultAddress.h" +#include "RigMainGrid.h" + +#include "RimCellFilterTools.h" +#include "RimCellRangeFilter.h" +#include "RimEclipsePropertyFilter.h" +#include "RimEclipseResultCase.h" +#include "RimEclipseResultDefinition.h" + +#include "cafPdmField.h" + +#include +#include + +#include +#include + +static std::unique_ptr openBruggeCase( 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; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RimCellFilterToolsTest, RangeFilterVisibility ) +{ + auto eclipseCase = openBruggeCase( "Real0", "BRUGGE_0000.EGRID" ); + ASSERT_TRUE( eclipseCase != nullptr ); + + RimCellRangeFilter rangeFilter; + rangeFilter.setCase( eclipseCase.get() ); + rangeFilter.startIndexI = 10; + rangeFilter.startIndexJ = 20; + rangeFilter.startIndexK = 1; + rangeFilter.cellCountI = 5; + rangeFilter.cellCountJ = 4; + rangeFilter.cellCountK = 3; + + auto visibility = RimCellFilterTools::computeReservoirCellVisibility( &rangeFilter, eclipseCase.get(), 0 ); + ASSERT_TRUE( visibility.notNull() ); + + const RigMainGrid* mainGrid = eclipseCase->eclipseCaseData()->mainGrid(); + ASSERT_EQ( mainGrid->totalCellCount(), visibility->size() ); + + size_t visibleCount = 0; + for ( size_t i = 0; i < visibility->size(); i++ ) + { + if ( visibility->val( i ) ) visibleCount++; + } + + // The Brugge grid has no LGRs, so the geometric mask is exactly the IJK box + EXPECT_EQ( size_t( 5 * 4 * 3 ), visibleCount ); + + // An EXCLUDE filter must select the complement + rangeFilter.setFilterMode( RimCellFilter::EXCLUDE ); + + auto excludeVisibility = RimCellFilterTools::computeReservoirCellVisibility( &rangeFilter, eclipseCase.get(), 0 ); + ASSERT_TRUE( excludeVisibility.notNull() ); + + size_t excludeVisibleCount = 0; + for ( size_t i = 0; i < excludeVisibility->size(); i++ ) + { + if ( excludeVisibility->val( i ) ) excludeVisibleCount++; + } + + EXPECT_EQ( visibility->size() - visibleCount, excludeVisibleCount ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RimCellFilterToolsTest, PropertyFilterVisibilityPerCase ) +{ + auto caseA = openBruggeCase( "Real0", "BRUGGE_0000.EGRID" ); + auto caseB = openBruggeCase( "Real40", "BRUGGE_0040.EGRID" ); + ASSERT_TRUE( caseA != nullptr ); + ASSERT_TRUE( caseB != nullptr ); + + const double lowerBound = 0.3; + const double upperBound = 0.6; + + // Property filter bound to case A + RimEclipsePropertyFilter propertyFilter; + propertyFilter.setCase( caseA.get() ); + propertyFilter.resultDefinition()->setEclipseCase( caseA.get() ); + propertyFilter.resultDefinition()->setResultType( RiaDefines::ResultCatType::DYNAMIC_NATIVE ); + propertyFilter.resultDefinition()->setResultVariable( RiaResultNames::swat() ); + + auto* lowerField = dynamic_cast*>( propertyFilter.findField( "LowerBound" ) ); + auto* upperField = dynamic_cast*>( propertyFilter.findField( "UpperBound" ) ); + ASSERT_TRUE( lowerField && upperField ); + lowerField->setValue( lowerBound ); + upperField->setValue( upperBound ); + + const size_t timeStepIndex = 5; + + auto countVisible = []( const cvf::UByteArray* visibility ) + { + size_t count = 0; + for ( size_t i = 0; i < visibility->size(); i++ ) + { + if ( visibility->val( i ) ) count++; + } + return count; + }; + + // Reference: count active cells with SWAT inside the bounds, using the case data directly + auto expectedVisibleCount = [&]( RimEclipseCase* eclipseCase ) + { + auto porosityModel = RiaDefines::PorosityModelType::MATRIX_MODEL; + auto results = eclipseCase->results( porosityModel ); + + RigEclipseResultAddress swatAddress( RiaDefines::ResultCatType::DYNAMIC_NATIVE, RiaResultNames::swat() ); + results->ensureKnownResultLoaded( swatAddress ); + const auto& values = results->cellScalarResults( swatAddress, timeStepIndex ); + + size_t count = 0; + for ( double value : values ) + { + if ( lowerBound <= value && value <= upperBound ) count++; + } + return count; + }; + + auto visibilityA = RimCellFilterTools::computeReservoirCellVisibility( &propertyFilter, caseA.get(), timeStepIndex ); + auto visibilityB = RimCellFilterTools::computeReservoirCellVisibility( &propertyFilter, caseB.get(), timeStepIndex ); + ASSERT_TRUE( visibilityA.notNull() ); + ASSERT_TRUE( visibilityB.notNull() ); + + // The same filter definition must be evaluated against each case's own result values + EXPECT_EQ( expectedVisibleCount( caseA.get() ), countVisible( visibilityA.p() ) ); + EXPECT_EQ( expectedVisibleCount( caseB.get() ), countVisible( visibilityB.p() ) ); + + // The realizations have different SWAT fields, so the masks must differ + ASSERT_EQ( visibilityA->size(), visibilityB->size() ); + size_t differingCells = 0; + for ( size_t i = 0; i < visibilityA->size(); i++ ) + { + if ( visibilityA->val( i ) != visibilityB->val( i ) ) differingCells++; + } + EXPECT_GT( differingCells, 0u ); +} From 07a3973b15c3d6fa8b8a7e16d3b2e8ca2a3ad7e8 Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Wed, 15 Jul 2026 15:10:19 +0200 Subject: [PATCH 07/12] #14345 Grid Calculation: add filtering with view-independent data filters Add a filter type option to the grid calculation: None, Cell Filter View or Data Filter. In data filter mode a single filter from the destination case's Data Filters collection is selected, and the filter is evaluated per calculation case and time step using each case's own active cells and result values. Property based filtering of ensemble calculations then uses each realization's own property values, which is not possible with view based filtering. Old projects with a cell filter view are migrated to the cell filter view mode. Complex filtering is composed with a combined filter (AND/OR) in the Data Filters collection. --- .../ProjectDataModel/RimGridCalculation.cpp | 89 +++++++++++++++++-- .../ProjectDataModel/RimGridCalculation.h | 11 +++ 2 files changed, 91 insertions(+), 9 deletions(-) diff --git a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp index 518f3094e16..bda93edb707 100644 --- a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp @@ -38,6 +38,9 @@ #include "RigStatisticsTools.h" #include "RimCaseCollection.h" +#include "RimCellFilter.h" +#include "RimCellFilterTools.h" +#include "RimDataFilterCollection.h" #include "RimEclipseCase.h" #include "RimEclipseCaseCollection.h" #include "RimEclipseCaseEnsemble.h" @@ -84,6 +87,14 @@ void caf::AppEnum::setUp() addItem( RimGridCalculation::AdditionalCasesType::ALL_CASES, "NONE", "All Cases" ); setDefault( RimGridCalculation::AdditionalCasesType::NONE ); } +template <> +void caf::AppEnum::setUp() +{ + addItem( RimGridCalculation::FilterType::NO_FILTER, "NO_FILTER", "None" ); + addItem( RimGridCalculation::FilterType::CELL_FILTER_VIEW, "CELL_FILTER_VIEW", "Cell Filter View" ); + addItem( RimGridCalculation::FilterType::DATA_FILTER, "DATA_FILTER", "Data Filter" ); + setDefault( RimGridCalculation::FilterType::NO_FILTER ); +} }; // namespace caf //-------------------------------------------------------------------------------------------------- @@ -92,7 +103,9 @@ void caf::AppEnum::setUp() RimGridCalculation::RimGridCalculation() { CAF_PDM_InitObject( "RimGridCalculation", ":/octave.png", "Calculation", "" ); + CAF_PDM_InitFieldNoDefault( &m_filterType, "FilterType", "Filter Type" ); CAF_PDM_InitFieldNoDefault( &m_cellFilterView, "VisibleCellView", "Filter by 3d View Visibility" ); + CAF_PDM_InitFieldNoDefault( &m_dataFilter, "DataFilter", "Data Filter" ); CAF_PDM_InitFieldNoDefault( &m_defaultValueType, "DefaultValueType", "Non-visible Cell Value" ); CAF_PDM_InitField( &m_defaultValue, "DefaultValue", 0.0, "Custom Value" ); CAF_PDM_InitFieldNoDefault( &m_destinationCase, "DestinationCase", "Destination Case" ); @@ -174,9 +187,18 @@ RimGridCalculationVariable* RimGridCalculation::createVariable() //-------------------------------------------------------------------------------------------------- bool RimGridCalculation::calculate() { + const bool useCellFilterView = ( m_filterType() == FilterType::CELL_FILTER_VIEW ) && m_cellFilterView() != nullptr; + const bool useDataFilter = ( m_filterType() == FilterType::DATA_FILTER ); + + if ( useDataFilter && m_dataFilter() == nullptr ) + { + RiuMessageDialog::showError( nullptr, "Grid Property Calculator", "The filter type is 'Data Filter', but no data filter is selected." ); + return false; + } + // Equal grid size is required if there is more than one grid case in the expression. If a cell filter view is active, the visibility is - // based on one view and reused for all other grid models, and requires equal grid size. - bool checkIfGridSizeIsEqual = ( !allSourceCasesAreEqualToDestinationCase() || m_cellFilterView != nullptr ) && + // based on one view and reused for all other grid models, and requires equal grid size. A data filter is evaluated per case. + bool checkIfGridSizeIsEqual = ( !allSourceCasesAreEqualToDestinationCase() || useCellFilterView ) && m_additionalCasesType != AdditionalCasesType::ENSEMBLE; for ( auto calculationCase : outputEclipseCases() ) @@ -244,7 +266,7 @@ bool RimGridCalculation::calculate() } cvf::ref inputValueVisibilityFilter; - if ( m_cellFilterView() ) + if ( useCellFilterView ) { if ( auto eclipseView = dynamic_cast( m_cellFilterView() ) ) { @@ -365,9 +387,14 @@ void RimGridCalculation::defineUiOrdering( QString uiConfigName, caf::PdmUiOrder caf::PdmUiGroup* filterGroup = uiOrdering.addNewGroup( "Cell Filter" ); filterGroup->setCollapsedByDefault(); - filterGroup->add( &m_cellFilterView ); + filterGroup->add( &m_filterType ); - if ( m_cellFilterView() != nullptr ) + if ( m_filterType() == FilterType::CELL_FILTER_VIEW ) filterGroup->add( &m_cellFilterView ); + if ( m_filterType() == FilterType::DATA_FILTER ) filterGroup->add( &m_dataFilter ); + + const bool hasFilter = ( m_filterType() == FilterType::CELL_FILTER_VIEW && m_cellFilterView() != nullptr ) || + ( m_filterType() == FilterType::DATA_FILTER && m_dataFilter() != nullptr ); + if ( hasFilter ) { filterGroup->add( &m_defaultValueType ); @@ -423,6 +450,19 @@ QList RimGridCalculation::calculateValueOptions( const c } } } + else if ( fieldNeedingOptions == &m_dataFilter ) + { + options.push_back( caf::PdmOptionItemInfo( "None", nullptr ) ); + + if ( m_destinationCase() && m_destinationCase()->dataFilterCollection() ) + { + for ( RimCellFilter* filter : m_destinationCase()->dataFilterCollection()->filters() ) + { + if ( !filter ) continue; + options.push_back( caf::PdmOptionItemInfo( filter->fullName(), filter, false, filter->uiIconProvider() ) ); + } + } + } else if ( fieldNeedingOptions == &m_destinationCase ) { RimEclipseCase* firstInputCase = nullptr; @@ -507,6 +547,27 @@ void RimGridCalculation::initAfterRead() } if ( m_applyToAllCases_OBSOLETE ) m_additionalCasesType = RimGridCalculation::AdditionalCasesType::ALL_CASES; + + // Projects from before the filter type field was introduced only have the cell filter view. The view + // pointer is cleared when the filter type is changed away from CELL_FILTER_VIEW, so a set view pointer + // implies the cell filter view mode. + if ( m_cellFilterView() != nullptr ) m_filterType = FilterType::CELL_FILTER_VIEW; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCalculation::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) +{ + RimUserDefinedCalculation::fieldChangedByUi( changedField, oldValue, newValue ); + + if ( changedField == &m_filterType ) + { + if ( m_filterType() != FilterType::CELL_FILTER_VIEW ) m_cellFilterView = nullptr; + if ( m_filterType() != FilterType::DATA_FILTER ) m_dataFilter = nullptr; + + updateConnectedEditors(); + } } //-------------------------------------------------------------------------------------------------- @@ -944,6 +1005,16 @@ bool RimGridCalculation::calculateForCases( const std::vector& // Skip time steps that are not in the list of time steps to calculate if ( timeSteps && std::find( timeSteps->begin(), timeSteps->end(), tsId ) == timeSteps->end() ) continue; + // A data filter is evaluated per calculation case and time step, using the case's own active + // cells and result values. The visibility filter provided by the caller is used as-is. + cvf::ref dataFilterVisibility; + cvf::UByteArray* visibilityFilter = inputValueVisibilityFilter; + if ( m_filterType() == FilterType::DATA_FILTER && m_dataFilter() ) + { + dataFilterVisibility = RimCellFilterTools::computeReservoirCellVisibility( m_dataFilter(), calculationCase, tsId ); + visibilityFilter = dataFilterVisibility.p(); + } + std::vector> dataForAllVariables; for ( size_t i = 0; i < m_variables.size(); i++ ) { @@ -963,11 +1034,11 @@ bool RimGridCalculation::calculateForCases( const std::vector& { const double defaultValue = 0.0; size_t nonVisibleCount = 0; - if ( inputValueVisibilityFilter ) + if ( visibilityFilter ) { auto activeCellInfo = calculationCase->eclipseCaseData()->activeCellInfo( porosityModel ); nonVisibleCount = - replaceFilteredValuesWithDefaultValue( defaultValue, inputValueVisibilityFilter, dataForVariable, activeCellInfo ); + replaceFilteredValuesWithDefaultValue( defaultValue, visibilityFilter, dataForVariable, activeCellInfo ); } // Aggregation functions include all values in the vector. Replace undefined values with the @@ -1028,9 +1099,9 @@ bool RimGridCalculation::calculateForCases( const std::vector& } } - if ( inputValueVisibilityFilter && !resultValues.empty() ) + if ( visibilityFilter && !resultValues.empty() ) { - filterResults( inputValueVisibilityFilter, + filterResults( visibilityFilter, dataForAllVariables, tsId, m_defaultValueType(), diff --git a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h index 1a3a127e422..ecd096cb7fb 100644 --- a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h +++ b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h @@ -30,6 +30,7 @@ #include +class RimCellFilter; class RimEclipseCase; class RimGridView; class RigEclipseResultAddress; @@ -61,6 +62,13 @@ class RimGridCalculation : public RimUserDefinedCalculation ALL_CASES }; + enum class FilterType + { + NO_FILTER, + CELL_FILTER_VIEW, + DATA_FILTER + }; + RimGridCalculation(); bool preCalculate() const override; @@ -133,6 +141,7 @@ class RimGridCalculation : public RimUserDefinedCalculation void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; QList calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override; void initAfterRead() override; + void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override; private: void onVariableUpdated( const SignalEmitter* emitter ); @@ -142,7 +151,9 @@ class RimGridCalculation : public RimUserDefinedCalculation static std::pair createStatisticsText( const std::vector>& values ); private: + caf::PdmField> m_filterType; caf::PdmPtrField m_cellFilterView; + caf::PdmPtrField m_dataFilter; caf::PdmField> m_defaultValueType; caf::PdmField m_defaultValue; caf::PdmPtrField m_destinationCase; From 861ae17964ef15e277778d875b038e80b7e2fefb Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Wed, 15 Jul 2026 15:15:11 +0200 Subject: [PATCH 08/12] #14345 Add polygon filter creation to the Data Filters menu RicNewPolygonFilterFeature already supports adding polygon filters to the case-level data filter collection, but the collection menu did not offer it. Add the polygon filter entries to the menu, matching the cell filter collection menu in a view. The polygon filter computes its cells lazily from the collection's source case, so no view is required. --- .../CellFilters/RimDataFilterCollection.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.cpp b/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.cpp index 1df81361430..e8de5672c20 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.cpp +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.cpp @@ -18,11 +18,15 @@ #include "RimDataFilterCollection.h" +#include "Polygons/RimPolygon.h" +#include "Polygons/RimPolygonCollection.h" + #include "RimCase.h" #include "RimCellRangeFilter.h" #include "RimCombinedFilter.h" #include "RimEclipsePropertyFilter.h" #include "RimEclipseResultDefinition.h" +#include "RimTools.h" #include "cafCmdFeatureMenuBuilder.h" #include "cafPdmFieldScriptingCapability.h" @@ -193,6 +197,21 @@ void RimDataFilterCollection::appendMenuItems( caf::CmdFeatureMenuBuilder& menuB { menuBuilder << "RicEclipsePropertyFilterNewFeature"; menuBuilder << "Separator"; + + menuBuilder.subMenuStart( "Polygon Filter", QIcon( ":/CellFilter_Polygon.png" ) ); + { + auto polygonCollection = RimTools::polygonCollection(); + for ( auto p : polygonCollection->allPolygons() ) + { + if ( !p ) continue; + + menuBuilder.addCmdFeatureWithUserData( "RicNewPolygonFilterFeature", p->name(), QVariant::fromValue( static_cast( p ) ) ); + } + } + menuBuilder.subMenuEnd(); + + menuBuilder << "RicNewPolygonFilterFeature"; + menuBuilder << "Separator"; menuBuilder.subMenuStart( "Range Filter" ); menuBuilder << "RicNewRangeFilterSliceIFeature"; menuBuilder << "RicNewRangeFilterSliceJFeature"; From 7c4e2396001888a8dafd5afb16f67038d90c6c6a Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Wed, 15 Jul 2026 15:40:38 +0200 Subject: [PATCH 09/12] #14345 Add Data Filters to grid ensembles Add a data filter collection to RimReservoirGridEnsemble, so filters can be defined once for the ensemble instead of per realization. The filters are bound to the main case of the ensemble for configuration, and evaluated per case as elsewhere. The filter creation commands are available from a Data Filters submenu on the ensemble node, and the collection node is hidden while empty, matching the case-level behavior. The grid calculation data filter options include the filters of the ensemble the destination case belongs to. --- .../RicCellFilterFeatureTools.h | 12 +++-- .../RicNewPolygonFilterFeature.cpp | 5 ++- .../ProjectDataModel/RimGridCalculation.cpp | 28 ++++++++++-- .../RimReservoirGridEnsemble.cpp | 40 +++++++++++++++++ .../RimReservoirGridEnsemble.h | 8 ++++ .../UnitTests/RimCellFilterTools-Test.cpp | 44 +++++++++++++++++++ 6 files changed, 127 insertions(+), 10 deletions(-) diff --git a/ApplicationLibCode/Commands/CellFilterCommands/RicCellFilterFeatureTools.h b/ApplicationLibCode/Commands/CellFilterCommands/RicCellFilterFeatureTools.h index 963465c7e4b..475b8f77f56 100644 --- a/ApplicationLibCode/Commands/CellFilterCommands/RicCellFilterFeatureTools.h +++ b/ApplicationLibCode/Commands/CellFilterCommands/RicCellFilterFeatureTools.h @@ -28,6 +28,7 @@ #include "RimEclipseCase.h" #include "RimFilterInViewCollection.h" #include "RimGridView.h" +#include "RimReservoirGridEnsemble.h" #include "Riu3DMainWindowTools.h" @@ -84,10 +85,10 @@ inline RimCellFilterCollection* resolveTargetCellFilterCollection() } //-------------------------------------------------------------------------------------------------- -/// Resolve the case-level RimDataFilterCollection to target from the current selection: either the -/// data-filter collection node itself, or a selected RimEclipseCase (whose "Data Filters" node is -/// hidden while empty, so the case node is right-clicked to create the first filter). Returns -/// nullptr if neither is selected. +/// Resolve the RimDataFilterCollection to target from the current selection: either the data-filter +/// collection node itself, or a selected RimEclipseCase or RimReservoirGridEnsemble (whose "Data +/// Filters" node is hidden while empty, so the owner node is right-clicked to create the first +/// filter). Returns nullptr if none is selected. //-------------------------------------------------------------------------------------------------- inline RimDataFilterCollection* selectedDataFilterCollection() { @@ -97,6 +98,9 @@ inline RimDataFilterCollection* selectedDataFilterCollection() auto cases = caf::selectedObjectsByTypeStrict(); if ( !cases.empty() && cases.front() ) return cases.front()->dataFilterCollection(); + auto ensembles = caf::selectedObjectsByTypeStrict(); + if ( !ensembles.empty() && ensembles.front() ) return ensembles.front()->dataFilterCollection(); + return nullptr; } diff --git a/ApplicationLibCode/Commands/CellFilterCommands/RicNewPolygonFilterFeature.cpp b/ApplicationLibCode/Commands/CellFilterCommands/RicNewPolygonFilterFeature.cpp index 9997c72c25f..3896bf8f7f9 100644 --- a/ApplicationLibCode/Commands/CellFilterCommands/RicNewPolygonFilterFeature.cpp +++ b/ApplicationLibCode/Commands/CellFilterCommands/RicNewPolygonFilterFeature.cpp @@ -104,8 +104,9 @@ void RicNewPolygonFilterFeature::onActionTriggered( bool isChecked ) return; } - // If a case-level Data Filter Collection is selected, add polygon filters there. - auto* dataCollection = caf::SelectionManager::instance()->selectedItemOfType(); + // If a case or ensemble level Data Filter Collection is selected (or its owner node), add + // polygon filters there. + auto* dataCollection = RicCellFilterFeatureTools::selectedDataFilterCollection(); if ( dataCollection ) { if ( polygons.empty() ) polygons.push_back( nullptr ); diff --git a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp index bda93edb707..ead0899ce70 100644 --- a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp @@ -55,6 +55,7 @@ #include "RimOilField.h" #include "RimProject.h" #include "RimReloadCaseTools.h" +#include "RimReservoirGridEnsemble.h" #include "RimResultSelectionUi.h" #include "RimTools.h" @@ -454,12 +455,31 @@ QList RimGridCalculation::calculateValueOptions( const c { options.push_back( caf::PdmOptionItemInfo( "None", nullptr ) ); - if ( m_destinationCase() && m_destinationCase()->dataFilterCollection() ) + if ( m_destinationCase() ) { - for ( RimCellFilter* filter : m_destinationCase()->dataFilterCollection()->filters() ) + if ( auto dataFilterCollection = m_destinationCase()->dataFilterCollection() ) { - if ( !filter ) continue; - options.push_back( caf::PdmOptionItemInfo( filter->fullName(), filter, false, filter->uiIconProvider() ) ); + for ( RimCellFilter* filter : dataFilterCollection->filters() ) + { + if ( !filter ) continue; + options.push_back( caf::PdmOptionItemInfo( filter->fullName(), filter, false, filter->uiIconProvider() ) ); + } + } + + // Also offer the data filters of the grid ensemble the destination case belongs to, if any + if ( auto gridEnsemble = m_destinationCase()->firstAncestorOfType() ) + { + if ( auto dataFilterCollection = gridEnsemble->dataFilterCollection() ) + { + for ( RimCellFilter* filter : dataFilterCollection->filters() ) + { + if ( !filter ) continue; + options.push_back( caf::PdmOptionItemInfo( QString( "%1 : %2" ).arg( gridEnsemble->name(), filter->fullName() ), + filter, + false, + filter->uiIconProvider() ) ); + } + } } } } diff --git a/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.cpp b/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.cpp index 1c878e4a2bc..e58e2832c50 100644 --- a/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.cpp @@ -18,6 +18,8 @@ #include "RimReservoirGridEnsemble.h" +#include "cafPdmUiTreeOrdering.h" + #include "RiaLogging.h" #include "RiaQStringFormatter.h" #include "RiaResultNames.h" @@ -37,6 +39,7 @@ #include "Formations/RimFormationNamesCollection.h" #include "Rim2dIntersectionViewCollection.h" #include "RimCaseCollection.h" +#include "RimDataFilterCollection.h" #include "RimEclipseCase.h" #include "RimEclipseCellColors.h" #include "RimEclipseResultCase.h" @@ -108,6 +111,9 @@ RimReservoirGridEnsemble::RimReservoirGridEnsemble() m_statisticsCaseCollection->uiCapability()->setUiName( "Derived Statistics" ); m_statisticsCaseCollection->uiCapability()->setUiIconFromResourceString( ":/Histograms16x16.png" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_dataFilterCollection, "DataFilterCollection", "Data Filters" ); + m_dataFilterCollection = new RimDataFilterCollection; + CAF_PDM_InitFieldNoDefault( &m_viewCollection, "ViewCollection", "Views" ); m_viewCollection = new RimEclipseViewCollection; m_viewCollection->setEclipseCaseProvider( [this]() { return this->cases(); } ); @@ -192,6 +198,7 @@ void RimReservoirGridEnsemble::addCase( RimEclipseCase* reservoir ) clearActiveCellUnions(); clearStatisticsResults(); updateMainGridAndActiveCellsForStatisticsCases(); + updateDataFilterCollectionCase(); } //-------------------------------------------------------------------------------------------------- @@ -211,6 +218,7 @@ void RimReservoirGridEnsemble::removeCase( RimEclipseCase* reservoir ) clearActiveCellUnions(); clearStatisticsResults(); updateMainGridAndActiveCellsForStatisticsCases(); + updateDataFilterCollectionCase(); } //-------------------------------------------------------------------------------------------------- @@ -634,6 +642,13 @@ void RimReservoirGridEnsemble::appendMenuItems( caf::CmdFeatureMenuBuilder& menu { menuBuilder << "RicNewStatisticsCaseFeature"; } + + // The "Data Filters" collection node is hidden while empty, so expose the filter creation + // commands on the ensemble node to allow creating the first filter. + menuBuilder << "Separator"; + menuBuilder.subMenuStart( "Data Filters", QIcon( ":/CellFilter.png" ) ); + m_dataFilterCollection->appendMenuItems( menuBuilder ); + menuBuilder.subMenuEnd(); } //-------------------------------------------------------------------------------------------------- @@ -645,6 +660,24 @@ RimFormationNames* RimReservoirGridEnsemble::activeFormationNames() const return m_activeFormationNames(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimDataFilterCollection* RimReservoirGridEnsemble::dataFilterCollection() const +{ + return m_dataFilterCollection(); +} + +//-------------------------------------------------------------------------------------------------- +/// The filters in the collection need a source case for configuration (result meta data, grid +/// geometry). Use the main case of the ensemble. Evaluation of the filters is done per case, see +/// RimCellFilterTools::computeReservoirCellVisibility. +//-------------------------------------------------------------------------------------------------- +void RimReservoirGridEnsemble::updateDataFilterCollectionCase() +{ + if ( m_dataFilterCollection() ) m_dataFilterCollection->setCase( mainCase() ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -711,6 +744,12 @@ void RimReservoirGridEnsemble::defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiT uiTreeOrdering.add( &m_caseCollection ); uiTreeOrdering.add( &m_statisticsCaseCollection ); + + if ( m_dataFilterCollection() && m_dataFilterCollection()->shouldBeVisibleInTree() ) + { + uiTreeOrdering.add( m_dataFilterCollection() ); + } + uiTreeOrdering.add( &m_viewCollection ); for ( auto eclipseCase : cases() ) @@ -779,6 +818,7 @@ void RimReservoirGridEnsemble::initAfterRead() m_viewCollection->setEclipseCaseProvider( [this]() { return this->cases(); } ); } + updateDataFilterCollectionCase(); updateStatisticsVisibility(); } diff --git a/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.h b/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.h index a4e04e490fd..b62c0ac0298 100644 --- a/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.h +++ b/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.h @@ -35,6 +35,7 @@ class RigActiveCellInfo; class RigMainGrid; class RimCaseCollection; +class RimDataFilterCollection; class RimEclipseCase; class RimEclipseStatisticsCase; class RimEclipseView; @@ -97,6 +98,9 @@ class RimReservoirGridEnsemble : public RimNamedObject, public RimReservoirGridE // Formation names RimFormationNames* activeFormationNames() const override; + // Data filters + RimDataFilterCollection* dataFilterCollection() const; + // Statistics RimCaseCollection* statisticsCaseCollection() const override; RimEclipseStatisticsCase* createAndAppendStatisticsCase() override; @@ -143,6 +147,7 @@ class RimReservoirGridEnsemble : public RimNamedObject, public RimReservoirGridE void loadGridsInSharedMode(); void loadGridsInIndividualMode(); void updateGridModeToolTip(); + void updateDataFilterCollectionCase(); private: // File set reference @@ -158,6 +163,9 @@ class RimReservoirGridEnsemble : public RimNamedObject, public RimReservoirGridE caf::PdmChildField m_caseCollection; caf::PdmChildField m_statisticsCaseCollection; + // Data filters + caf::PdmChildField m_dataFilterCollection; + // Grid mode caf::PdmField m_autoDetectGridType; caf::PdmField> m_gridMode; diff --git a/ApplicationLibCode/UnitTests/RimCellFilterTools-Test.cpp b/ApplicationLibCode/UnitTests/RimCellFilterTools-Test.cpp index 597f78247e1..25578f08c73 100644 --- a/ApplicationLibCode/UnitTests/RimCellFilterTools-Test.cpp +++ b/ApplicationLibCode/UnitTests/RimCellFilterTools-Test.cpp @@ -30,9 +30,11 @@ #include "RimCellFilterTools.h" #include "RimCellRangeFilter.h" +#include "RimDataFilterCollection.h" #include "RimEclipsePropertyFilter.h" #include "RimEclipseResultCase.h" #include "RimEclipseResultDefinition.h" +#include "RimReservoirGridEnsemble.h" #include "cafPdmField.h" @@ -104,6 +106,48 @@ TEST( RimCellFilterToolsTest, RangeFilterVisibility ) EXPECT_EQ( visibility->size() - visibleCount, excludeVisibleCount ); } +//-------------------------------------------------------------------------------------------------- +/// Data filters in a grid ensemble collection are bound to the main case of the ensemble, and can +/// be evaluated per case without any view +//-------------------------------------------------------------------------------------------------- +TEST( RimCellFilterToolsTest, GridEnsembleDataFilterCollection ) +{ + auto eclipseCase = openBruggeCase( "Real0", "BRUGGE_0000.EGRID" ); + ASSERT_TRUE( eclipseCase != nullptr ); + + auto ensemble = std::make_unique(); + + // The ensemble takes ownership of the case + RimEclipseResultCase* mainCase = eclipseCase.release(); + ensemble->addCase( mainCase ); + + ASSERT_TRUE( ensemble->dataFilterCollection() != nullptr ); + + // New filters in the ensemble collection are bound to the main case of the ensemble + auto* propertyFilter = ensemble->dataFilterCollection()->addNewPropertyFilter(); + ASSERT_TRUE( propertyFilter != nullptr ); + EXPECT_EQ( mainCase, propertyFilter->resultDefinition()->eclipseCase() ); + + auto* rangeFilter = ensemble->dataFilterCollection()->addNewRangeFilter(); + ASSERT_TRUE( rangeFilter != nullptr ); + rangeFilter->startIndexI = 1; + rangeFilter->startIndexJ = 1; + rangeFilter->startIndexK = 1; + rangeFilter->cellCountI = 10; + rangeFilter->cellCountJ = 10; + rangeFilter->cellCountK = 1; + + auto visibility = RimCellFilterTools::computeReservoirCellVisibility( rangeFilter, mainCase, 0 ); + ASSERT_TRUE( visibility.notNull() ); + + size_t visibleCount = 0; + for ( size_t i = 0; i < visibility->size(); i++ ) + { + if ( visibility->val( i ) ) visibleCount++; + } + EXPECT_EQ( size_t( 10 * 10 * 1 ), visibleCount ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- From cc215fd1d5d9d80084f296250c986f6f0a31b414 Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Wed, 15 Jul 2026 15:47:26 +0200 Subject: [PATCH 10/12] #14345 Refresh the owning object when data filters are added or removed The data filter collection node is hidden in the project tree while the collection is empty, and the owner must be refreshed to re-run its tree ordering when the first filter appears. The refresh targeted the source case, which for a grid ensemble collection is the main case of the ensemble, not the ensemble owning the tree node. Newly created ensemble data filters were therefore not visible in the tree. Refresh the PDM parent object instead, which is the case or the grid ensemble. Also refresh from the programmatic removeFilter path. --- .../CellFilters/RimDataFilterCollection.cpp | 23 +++++++++++++++---- .../CellFilters/RimDataFilterCollection.h | 1 + 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.cpp b/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.cpp index e8de5672c20..82fd2c8ca56 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.cpp +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.cpp @@ -84,6 +84,7 @@ void RimDataFilterCollection::removeFilter( RimCellFilter* f ) if ( !f ) return; deleteItem( f ); filtersChanged.send(); + updateOwnerEditors(); } //-------------------------------------------------------------------------------------------------- @@ -161,9 +162,9 @@ void RimDataFilterCollection::onItemsChanged() } filtersChanged.send(); - // The collection node is hidden from the case tree while empty, so refresh the owner case to + // The collection node is hidden from the owner's tree while empty, so refresh the owner to // re-run its defineUiTreeOrdering and add the node once the first filter appears. - if ( auto* c = ownerCase() ) c->updateConnectedEditors(); + updateOwnerEditors(); } //-------------------------------------------------------------------------------------------------- @@ -175,8 +176,22 @@ void RimDataFilterCollection::onChildDeleted( caf::PdmChildArrayFieldHandle* chi updateConnectedEditors(); filtersChanged.send(); - // Refresh the owner case so its defineUiTreeOrdering re-runs and drops the node once empty. - if ( auto* c = ownerCase() ) c->updateConnectedEditors(); + // Refresh the owner so its defineUiTreeOrdering re-runs and drops the node once empty. + updateOwnerEditors(); +} + +//-------------------------------------------------------------------------------------------------- +/// Refresh the object owning this collection in the project tree. Note that the owner is not +/// necessarily the source case: for a collection in a grid ensemble, the source case is the main +/// case of the ensemble, while the tree node is owned by the ensemble. +//-------------------------------------------------------------------------------------------------- +void RimDataFilterCollection::updateOwnerEditors() +{ + caf::PdmObjectHandle* owner = parentField() ? parentField()->ownerObject() : nullptr; + if ( owner && owner->uiCapability() ) + { + owner->uiCapability()->updateConnectedEditors(); + } } //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.h b/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.h index d9a84252f17..c0e9c43f135 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.h +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.h @@ -72,6 +72,7 @@ class RimDataFilterCollection : public caf::PdmObjectCollection private: void connectChildSignal( RimCellFilter* child ); void onChildFilterChanged( const caf::SignalEmitter* emitter ); + void updateOwnerEditors(); caf::PdmPtrField m_srcCase; }; From b2b5462c38f409427cd37f6f692d737391f75ff8 Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Wed, 15 Jul 2026 16:30:43 +0200 Subject: [PATCH 11/12] #14345 Restore view visibility for the cell filter view filter type The cell filter view filter type was changed to use the cell filter geometry to include cells inside the filters that are inactive in the view's case, at the cost of ignoring property filters in the view. That use case is now covered by the data filter type, which evaluates the filters per calculation case. Restore the original visible cells semantics for the cell filter view filter type, so property filters in the view affect the calculation again. --- .../ProjectDataModel/RimGridCalculation.cpp | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp index ead0899ce70..c7bfd09799a 100644 --- a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp @@ -266,23 +266,13 @@ bool RimGridCalculation::calculate() } } + // The visible cells of the view, including the effect of property filters evaluated on the view's + // case. Cells inside the filters that are inactive in the view's case are not part of the mask; use + // the data filter type to evaluate filters per calculation case. cvf::ref inputValueVisibilityFilter; if ( useCellFilterView ) { - if ( auto eclipseView = dynamic_cast( m_cellFilterView() ) ) - { - // Use the cell filter geometry, independent of the active cells in the view's case. Cells inside the - // filters that are inactive in the view's case can be active in other calculation cases, and must be - // included when the calculation is applied to additional cases. - inputValueVisibilityFilter = new cvf::UByteArray; - eclipseView->calculateCellVisibility( inputValueVisibilityFilter.p(), - { RANGE_FILTERED, RANGE_FILTERED_INACTIVE }, - eclipseView->currentTimeStep() ); - } - else - { - inputValueVisibilityFilter = m_cellFilterView()->currentTotalCellVisibility(); - } + inputValueVisibilityFilter = m_cellFilterView()->currentTotalCellVisibility(); } std::optional> timeSteps = std::nullopt; From 604d498a8184fa7c1565dccfacc3727471e171d5 Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Fri, 7 Aug 2026 16:26:17 +0200 Subject: [PATCH 12/12] #14345 Propagate LGR parent visibility for index-based data filters The grid calculation data filter evaluated index filters (polygon, user defined) directly on every grid. An INDEX_K polygon evaluated on a fine LGR can select no cells, so refined cells whose parent host cell is inside the polygon were dropped from the aggregation. This gave a lower sum than the histogram of visible cells in the 3d view, which renders LGR cells following their parent grid cell. Hoist the parent grid visibility propagation out of the range filter branch so it also runs for index filters. Property filters are evaluated per cell against their own result values and are left untouched. --- .../CellFilters/RimCellFilterTools.cpp | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilterTools.cpp b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilterTools.cpp index 10ae5d706f4..4465ba84bd0 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilterTools.cpp +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilterTools.cpp @@ -56,8 +56,7 @@ cvf::ref if ( filter->isRangeFilter() ) { // Range filters evaluate only on their target grid. On other grids an INCLUDE filter - // contributes no cells, while an EXCLUDE filter removes none. Cells in LGRs follow the - // visibility of their parent grid cell, as in the filtered geometry of a 3d view. + // contributes no cells, while an EXCLUDE filter removes none. const bool isTargetGrid = ( filter->gridIndex() == static_cast( gridIndex ) ); if ( isTargetGrid ) { @@ -67,25 +66,6 @@ cvf::ref { gridMask.setAll( !isInclude ); } - - if ( !grid->isMainGrid() ) - { - auto localGrid = static_cast( grid ); - const cvf::UByteArray& parentMask = *gridMasks[localGrid->parentGrid()->gridIndex()]; - - for ( size_t localIdx = 0; localIdx < grid->cellCount(); localIdx++ ) - { - const size_t parentCellIndex = grid->cell( localIdx ).parentCellIndex(); - if ( isInclude ) - { - gridMask[localIdx] = gridMask[localIdx] || parentMask[parentCellIndex]; - } - else - { - gridMask[localIdx] = gridMask[localIdx] && parentMask[parentCellIndex]; - } - } - } } else { @@ -93,6 +73,30 @@ cvf::ref filter->applyToCellVisibility( &gridMask, grid, timeStepIndex, eclipseCase ); } + // Cells in LGRs follow the visibility of their parent grid cell, as in the filtered geometry + // of a 3d view. Geometry based filters (range and index, e.g. an INDEX_K polygon) can fail to + // select the refined cells directly on a fine LGR, so propagate the parent grid visibility. + // Property filters evaluate each cell against its own result value and must be left untouched. + const bool isGeometryFilter = filter->isRangeFilter() || filter->isIndexFilter(); + if ( isGeometryFilter && !grid->isMainGrid() ) + { + auto localGrid = static_cast( grid ); + const cvf::UByteArray& parentMask = *gridMasks[localGrid->parentGrid()->gridIndex()]; + + for ( size_t localIdx = 0; localIdx < grid->cellCount(); localIdx++ ) + { + const size_t parentCellIndex = grid->cell( localIdx ).parentCellIndex(); + if ( isInclude ) + { + gridMask[localIdx] = gridMask[localIdx] || parentMask[parentCellIndex]; + } + else + { + gridMask[localIdx] = gridMask[localIdx] && parentMask[parentCellIndex]; + } + } + } + for ( size_t localIdx = 0; localIdx < grid->cellCount(); localIdx++ ) { reservoirVisibility->set( grid->reservoirCellIndex( localIdx ), gridMask[localIdx] );