From 7b580694edbdb03a053bbb3353a94d4976115b17 Mon Sep 17 00:00:00 2001 From: Magne Sjaastad Date: Thu, 22 Oct 2026 08:38:50 +0200 Subject: [PATCH] #13430: Exclude zero values from ensemble statistics for derived vectors Add m_includeZeroValues option to RimEnsembleStatistics (default on, preserving existing behavior). When disabled, exact zero values (e.g. from shut-in wells/groups) are excluded from the mean, P10/P50/P90 and custom percentile calculations in RimEnsembleStatisticsCase::calculate(). This is useful for derived vectors such as WCT, GOR and PI, where zeros caused by a shut well/group should not skew the statistical curves. Exact equality (not an epsilon) is used, since shut-in values are reported as exact 0.0 by the simulator and non-accumulated vectors use STEP_RIGHT interpolation, which does not introduce floating-point noise. --- .../Summary/RimEnsembleCurveSet.cpp | 6 +- .../Summary/RimEnsembleStatistics.cpp | 10 ++ .../Summary/RimEnsembleStatistics.h | 3 + .../Summary/RimEnsembleStatisticsCase.cpp | 15 +- .../Summary/RimEnsembleStatisticsCase.h | 3 +- ApplicationLibCode/UnitTests/CMakeLists.txt | 1 + .../RimEnsembleStatisticsCase-Test.cpp | 130 ++++++++++++++++++ 7 files changed, 164 insertions(+), 4 deletions(-) create mode 100644 ApplicationLibCode/UnitTests/RimEnsembleStatisticsCase-Test.cpp diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.cpp index 5ef0726bc9..d2964279ce 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.cpp @@ -2233,7 +2233,11 @@ void RimEnsembleCurveSet::updateStatisticsCurves( const std::vectorcalculate( statCases, summaryAddressY(), m_statistics->includeIncompleteCurves(), percentiles ); + m_ensembleStatCaseY->calculate( statCases, + summaryAddressY(), + m_statistics->includeIncompleteCurves(), + percentiles, + m_statistics->includeZeroValues() ); } } diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatistics.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatistics.cpp index 8b207c4385..bd4605a647 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatistics.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatistics.cpp @@ -61,6 +61,7 @@ RimEnsembleStatistics::RimEnsembleStatistics( RimEnsembleCurveSetInterface* pare m_customPercentiles.setCustomValidationCallback( [this]() -> QString { return validateCustomPercentiles(); } ); CAF_PDM_InitField( &m_showCurveLabels, "ShowCurveLabels", true, "Show Curve Labels" ); CAF_PDM_InitField( &m_includeIncompleteCurves, "IncludeIncompleteCurves", false, "Include Incomplete Curves" ); + CAF_PDM_InitField( &m_includeZeroValues, "IncludeZeroValues", true, "Include Zero Values" ); CAF_PDM_InitField( &m_crossPlotCurvesBinCount, "CrossPlotCurvesBinCount", 100, "Bin Count" ); CAF_PDM_InitField( &m_crossPlotCurvesStatisticsRealizationCountThresholdPerBin, @@ -241,6 +242,14 @@ bool RimEnsembleStatistics::includeIncompleteCurves() const return m_includeIncompleteCurves; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimEnsembleStatistics::includeZeroValues() const +{ + return m_includeZeroValues; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -302,6 +311,7 @@ void RimEnsembleStatistics::defaultUiOrdering( bool showCrossPlotGroup, caf::Pdm uiOrdering.add( &m_showEnsembleCurves ); uiOrdering.add( &m_basedOnFilteredCases ); uiOrdering.add( &m_includeIncompleteCurves ); + uiOrdering.add( &m_includeZeroValues ); uiOrdering.add( &m_showCurveLabels ); if ( showCrossPlotGroup ) diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatistics.h b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatistics.h index 53586b4c58..5318ce8efc 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatistics.h +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatistics.h @@ -55,6 +55,8 @@ public: void enableIncludeIncompleteCurves( bool enable ); + bool includeZeroValues() const; + cvf::Color3f color() const { return m_color; } void setColor( const cvf::Color3f& color ); bool customColor() const; @@ -93,6 +95,7 @@ private: caf::PdmField m_customPercentiles; caf::PdmField m_showCurveLabels; caf::PdmField m_includeIncompleteCurves; + caf::PdmField m_includeZeroValues; caf::PdmField m_customColor; // Ensemble cross plot settings diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatisticsCase.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatisticsCase.cpp index 351fc11e60..14e4329fb3 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatisticsCase.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatisticsCase.cpp @@ -168,9 +168,11 @@ RifSummaryReaderInterface* RimEnsembleStatisticsCase::summaryReader() void RimEnsembleStatisticsCase::calculate( const std::vector& summaryCases, const RifEclipseSummaryAddress& inputAddress, bool includeIncompleteCurves, - const std::vector& percentiles ) + const std::vector& percentiles, + bool includeZeroValues ) { - auto hash = RiaHashTools::hash( summaryCases, inputAddress.toEclipseTextAddress(), includeIncompleteCurves, percentiles ); + auto hash = + RiaHashTools::hash( summaryCases, inputAddress.toEclipseTextAddress(), includeIncompleteCurves, percentiles, includeZeroValues ); if ( hash == m_hash ) return; auto startTime = RiaLogging::currentTime(); @@ -266,6 +268,15 @@ void RimEnsembleStatisticsCase::calculate( const std::vector& s valuesAtTimeStep.push_back( curveValues[curveIdx][timeStepIndex] ); } + if ( !includeZeroValues ) + { + // Shut-in wells/groups report exact 0.0 for rate/ratio vectors (e.g. WCT, GOR, PI) from the simulator, and non-accumulated + // vectors use STEP_RIGHT interpolation above, which copies recorded values without introducing floating-point noise. An + // epsilon comparison is therefore not needed, and could incorrectly exclude small-but-valid values for low-magnitude + // vectors such as PI. + valuesAtTimeStep.erase( std::remove( valuesAtTimeStep.begin(), valuesAtTimeStep.end(), 0.0 ), valuesAtTimeStep.end() ); + } + double mean = RigStatisticsMath::calculateMean( valuesAtTimeStep ); m_meanData.push_back( mean ); diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatisticsCase.h b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatisticsCase.h index aeacddf2ee..f50f6f3f25 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatisticsCase.h +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatisticsCase.h @@ -49,7 +49,8 @@ public: void calculate( const std::vector& summaryCases, const RifEclipseSummaryAddress& inputAddress, bool includeIncompleteCurves, - const std::vector& percentiles = { 10, 50, 90 } ); + const std::vector& percentiles = { 10, 50, 90 }, + bool includeZeroValues = true ); std::vector timeSteps( const RifEclipseSummaryAddress& resultAddress ) const override; std::pair> values( const RifEclipseSummaryAddress& resultAddress ) const override; diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index ca8e4f5564..89c27298df 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -202,6 +202,7 @@ set(SOURCE_UNITTEST_FILES ${CMAKE_CURRENT_LIST_DIR}/RiaSummaryAddressCollectionTools-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/../TestModels/RimMockSummaryCase.h ${CMAKE_CURRENT_LIST_DIR}/RimMockSummaryCase-Test.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimEnsembleStatisticsCase-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RimSummaryCalculation-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifMultipleSummaryReaders-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RimSummaryCalculationCollection-Test.cpp diff --git a/ApplicationLibCode/UnitTests/RimEnsembleStatisticsCase-Test.cpp b/ApplicationLibCode/UnitTests/RimEnsembleStatisticsCase-Test.cpp new file mode 100644 index 0000000000..0b39c01aa4 --- /dev/null +++ b/ApplicationLibCode/UnitTests/RimEnsembleStatisticsCase-Test.cpp @@ -0,0 +1,130 @@ +#include "gtest/gtest.h" + +#include "RimEnsembleStatisticsCase.h" +#include "RimMockSummaryCase.h" + +#include "RifEclipseSummaryAddress.h" + +#include + +//-------------------------------------------------------------------------------------------------- +/// Three realizations with a zero value (well shut-in) at the second time step. With zero values +/// included, the mean at that time step is affected by the zero. With zero values excluded, the zero +/// should not contribute to the mean (or percentiles). +//-------------------------------------------------------------------------------------------------- +TEST( RimEnsembleStatisticsCase, IncludeZeroValuesAffectsMean ) +{ + auto addr = RifEclipseSummaryAddress::wellAddress( "WWCT", "W-1" ); + + RimMockSummaryCase caseA; + RimMockSummaryCase caseB; + RimMockSummaryCase caseC; + + std::vector timeSteps = { 0, 100, 200 }; + + caseA.addVector( addr, timeSteps, { 0.1, 0.0, 0.3 } ); + caseB.addVector( addr, timeSteps, { 0.2, 0.0, 0.4 } ); + caseC.addVector( addr, timeSteps, { 0.3, 0.6, 0.5 } ); + + std::vector summaryCases = { &caseA, &caseB, &caseC }; + + auto meanAddress = addr; + meanAddress.setStatisticsType( RifEclipseSummaryAddressDefines::StatisticsType::MEAN ); + + { + RimEnsembleStatisticsCase statisticsCase; + bool includeIncompleteCurves = true; + bool includeZeroValues = true; + statisticsCase.calculate( summaryCases, addr, includeIncompleteCurves, { 10, 50, 90 }, includeZeroValues ); + + auto [ok, meanValues] = statisticsCase.values( meanAddress ); + ASSERT_TRUE( ok ); + ASSERT_EQ( 3u, meanValues.size() ); + + // Mean of 0.0, 0.0, 0.6 at the second time step + EXPECT_NEAR( 0.2, meanValues[1], 1.0e-6 ); + } + + { + RimEnsembleStatisticsCase statisticsCase; + bool includeIncompleteCurves = true; + bool includeZeroValues = false; + statisticsCase.calculate( summaryCases, addr, includeIncompleteCurves, { 10, 50, 90 }, includeZeroValues ); + + auto [ok, meanValues] = statisticsCase.values( meanAddress ); + ASSERT_TRUE( ok ); + ASSERT_EQ( 3u, meanValues.size() ); + + // Mean of only 0.6 at the second time step, as the two zero values are excluded + EXPECT_NEAR( 0.6, meanValues[1], 1.0e-6 ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// If every realization has a zero value at a given time step and zero values are excluded, no data +/// remains for the statistics calculation at that time step. +//-------------------------------------------------------------------------------------------------- +TEST( RimEnsembleStatisticsCase, IncludeZeroValuesFalseWithAllZerosGivesInvalidMean ) +{ + auto addr = RifEclipseSummaryAddress::wellAddress( "WWCT", "W-1" ); + + RimMockSummaryCase caseA; + RimMockSummaryCase caseB; + + std::vector timeSteps = { 0, 100 }; + + caseA.addVector( addr, timeSteps, { 0.1, 0.0 } ); + caseB.addVector( addr, timeSteps, { 0.2, 0.0 } ); + + std::vector summaryCases = { &caseA, &caseB }; + + RimEnsembleStatisticsCase statisticsCase; + bool includeIncompleteCurves = true; + bool includeZeroValues = false; + statisticsCase.calculate( summaryCases, addr, includeIncompleteCurves, { 10, 50, 90 }, includeZeroValues ); + + auto meanAddress = addr; + meanAddress.setStatisticsType( RifEclipseSummaryAddressDefines::StatisticsType::MEAN ); + + auto [ok, meanValues] = statisticsCase.values( meanAddress ); + ASSERT_TRUE( ok ); + ASSERT_EQ( 2u, meanValues.size() ); + + EXPECT_NEAR( 0.15, meanValues[0], 1.0e-6 ); + EXPECT_EQ( std::numeric_limits::infinity(), meanValues[1] ); +} + +//-------------------------------------------------------------------------------------------------- +/// Values that are close to, but not exactly, zero are not treated as zero and are not excluded. Shut-in +/// values are reported as exact 0.0 by the simulator, so an exact comparison is used rather than an +/// epsilon, to avoid excluding small-but-valid values for e.g. low-magnitude vectors such as PI. +//-------------------------------------------------------------------------------------------------- +TEST( RimEnsembleStatisticsCase, IncludeZeroValuesFalseDoesNotExcludeNearZeroValues ) +{ + auto addr = RifEclipseSummaryAddress::wellAddress( "WWCT", "W-1" ); + + RimMockSummaryCase caseA; + RimMockSummaryCase caseB; + + std::vector timeSteps = { 0, 100 }; + + caseA.addVector( addr, timeSteps, { 0.1, 1.0e-9 } ); + caseB.addVector( addr, timeSteps, { 0.2, -1.0e-9 } ); + + std::vector summaryCases = { &caseA, &caseB }; + + RimEnsembleStatisticsCase statisticsCase; + bool includeIncompleteCurves = true; + bool includeZeroValues = false; + statisticsCase.calculate( summaryCases, addr, includeIncompleteCurves, { 10, 50, 90 }, includeZeroValues ); + + auto meanAddress = addr; + meanAddress.setStatisticsType( RifEclipseSummaryAddressDefines::StatisticsType::MEAN ); + + auto [ok, meanValues] = statisticsCase.values( meanAddress ); + ASSERT_TRUE( ok ); + ASSERT_EQ( 2u, meanValues.size() ); + + // The near-zero values are not excluded, since they are not exactly 0.0 + EXPECT_NEAR( 0.0, meanValues[1], 1.0e-9 ); +}