From a2b3445692674cd7f87a5f0ede92d6695e29f4b7 Mon Sep 17 00:00:00 2001 From: Magne Sjaastad Date: Wed, 11 Feb 2026 14:06:50 +0100 Subject: [PATCH] Add support for custom percentiles in ensemble statistics Introduced custom percentile handling for ensemble statistics: - Added "CUSTOM" statistics type and percentile field to RifEclipseSummaryAddress - Refactored RimEnsembleStatisticsCase to store and compute arbitrary percentiles - RimEnsembleStatistics now parses custom percentiles from UI input - RimEnsembleCurveSet and related interfaces updated to handle custom percentile curves and labels - Hashing and statistics calculations now include custom percentiles for cache validity --- .../Application/Tools/RiaHashTools.h | 17 +++ .../RifEclipseSummaryAddress.cpp | 41 ++++++- .../FileInterface/RifEclipseSummaryAddress.h | 7 ++ .../RifEclipseSummaryAddressDefines.cpp | 1 + .../RifEclipseSummaryAddressDefines.h | 3 +- .../RimEnsembleCurveSetInterface.h | 9 +- .../RimEnsembleCrossPlotStatisticsCase.cpp | 9 ++ .../RimEnsembleCrossPlotStatisticsCase.h | 1 + .../Summary/RimEnsembleCurveSet.cpp | 49 +++++++- .../Summary/RimEnsembleCurveSet.h | 1 + .../Summary/RimEnsembleStatistics.cpp | 81 +++++++++++++ .../Summary/RimEnsembleStatistics.h | 16 ++- .../Summary/RimEnsembleStatisticsCase.cpp | 107 ++++++++++++++---- .../Summary/RimEnsembleStatisticsCase.h | 17 ++- .../Summary/RimSummaryAddress.cpp | 38 ++++--- .../Summary/RimSummaryAddress.h | 1 + .../WellLog/RimEnsembleWellLogCurveSet.cpp | 9 ++ .../WellLog/RimEnsembleWellLogCurveSet.h | 1 + 18 files changed, 350 insertions(+), 58 deletions(-) diff --git a/ApplicationLibCode/Application/Tools/RiaHashTools.h b/ApplicationLibCode/Application/Tools/RiaHashTools.h index 8a50f80ed3..123ba48a15 100644 --- a/ApplicationLibCode/Application/Tools/RiaHashTools.h +++ b/ApplicationLibCode/Application/Tools/RiaHashTools.h @@ -21,6 +21,8 @@ #include #include #include +#include +#include //================================================================================================== // @@ -31,14 +33,29 @@ namespace RiaHashTools { //-------------------------------------------------------------------------------------------------- /// Variadic template function to combine multiple parameters into a single hash +/// Constrained to non-range types (std::string is excluded as it has std::hash specialization) //-------------------------------------------------------------------------------------------------- template + requires( !std::ranges::range || std::is_same_v, std::string> ) void combineHash( size_t& seed, const T& value ) { // Based on https://www.boost.org/doc/libs/1_84_0/libs/container_hash/doc/html/hash.html#notes_hash_combine seed ^= std::hash()( value ) + 0x9e3779b9 + ( seed << 6 ) + ( seed >> 2 ); } +//-------------------------------------------------------------------------------------------------- +/// Overload for range types (excluding std::string) +//-------------------------------------------------------------------------------------------------- +template + requires( !std::is_same_v, std::string> ) +void combineHash( size_t& seed, const Range& range ) +{ + for ( const auto& elem : range ) + { + combineHash( seed, elem ); + } +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/FileInterface/RifEclipseSummaryAddress.cpp b/ApplicationLibCode/FileInterface/RifEclipseSummaryAddress.cpp index 146fa503a6..06ab511256 100644 --- a/ApplicationLibCode/FileInterface/RifEclipseSummaryAddress.cpp +++ b/ApplicationLibCode/FileInterface/RifEclipseSummaryAddress.cpp @@ -44,6 +44,7 @@ RifEclipseSummaryAddress::RifEclipseSummaryAddress( SummaryCategory category, st , m_number2( -1 ) , m_isErrorResult( false ) , m_id( -1 ) + , m_percentile( -1 ) { std::pair reg2regPair; switch ( category ) @@ -132,6 +133,7 @@ RifEclipseSummaryAddress::RifEclipseSummaryAddress( SummaryCategory category, , m_number2( -1 ) , m_isErrorResult( isErrorResult ) , m_id( id ) + , m_percentile( -1 ) { switch ( category ) { @@ -196,6 +198,7 @@ RifEclipseSummaryAddress::RifEclipseSummaryAddress() , m_number2( -1 ) , m_isErrorResult( false ) , m_id( -1 ) + , m_percentile( -1 ) { } @@ -669,8 +672,17 @@ std::string RifEclipseSummaryAddress::uiText() const if ( isStatistics() ) { - auto prefix = RifEclipseSummaryAddressDefines::statisticsTypeToString( statisticsType() ); - text = prefix + ":" + text; + std::string prefix; + if ( statisticsType() == RifEclipseSummaryAddressDefines::StatisticsType::CUSTOM && m_percentile >= MIN_PERCENTILE && + m_percentile <= MAX_PERCENTILE ) + { + prefix = "P" + std::to_string( m_percentile ); + } + else + { + prefix = RifEclipseSummaryAddressDefines::statisticsTypeToString( statisticsType() ); + } + text = prefix + ":" + text; } return text; @@ -1027,6 +1039,31 @@ void RifEclipseSummaryAddress::setId( int id ) m_id = id; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +int RifEclipseSummaryAddress::percentile() const +{ + return m_percentile; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RifEclipseSummaryAddress::setPercentile( int percentile ) +{ + // Validate percentile value: must be in valid range [0, 100] or -1 (unset) + if ( ( percentile >= MIN_PERCENTILE && percentile <= MAX_PERCENTILE ) || percentile == -1 ) + { + m_percentile = percentile; + } + else + { + // Invalid percentile value, set to -1 (unset) + m_percentile = -1; + } +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/FileInterface/RifEclipseSummaryAddress.h b/ApplicationLibCode/FileInterface/RifEclipseSummaryAddress.h index a1e4827e91..9442c1015a 100644 --- a/ApplicationLibCode/FileInterface/RifEclipseSummaryAddress.h +++ b/ApplicationLibCode/FileInterface/RifEclipseSummaryAddress.h @@ -110,6 +110,8 @@ public: bool isStatistics() const; StatisticsType statisticsType() const; void setStatisticsType( StatisticsType type ); + int percentile() const; + void setPercentile( int percentile ); int regionNumber() const; int regionNumber2() const; @@ -166,6 +168,10 @@ public: std::string formatUiTextRegionToRegion() const; static std::pair regionToRegionPairFromUiText( const std::string& s ); + // Percentile constants + static constexpr int MIN_PERCENTILE = 0; + static constexpr int MAX_PERCENTILE = 100; + private: static RifEclipseSummaryAddress fromTokens( const std::vector& tokens ); @@ -188,6 +194,7 @@ private: int m_number2; bool m_isErrorResult; int m_id; + int m_percentile; }; QTextStream& operator<<( QTextStream& str, const RifEclipseSummaryAddress& sobj ); diff --git a/ApplicationLibCode/FileInterface/RifEclipseSummaryAddressDefines.cpp b/ApplicationLibCode/FileInterface/RifEclipseSummaryAddressDefines.cpp index a4e76c031d..d740f11bb5 100644 --- a/ApplicationLibCode/FileInterface/RifEclipseSummaryAddressDefines.cpp +++ b/ApplicationLibCode/FileInterface/RifEclipseSummaryAddressDefines.cpp @@ -30,6 +30,7 @@ void caf::AppEnum::setUp() addItem( RifEclipseSummaryAddressDefines::StatisticsType::P50, "P50", "P50" ); addItem( RifEclipseSummaryAddressDefines::StatisticsType::P90, "P90", "P90" ); addItem( RifEclipseSummaryAddressDefines::StatisticsType::MEAN, "MEAN", "Mean" ); + addItem( RifEclipseSummaryAddressDefines::StatisticsType::CUSTOM, "CUSTOM", "Custom" ); setDefault( RifEclipseSummaryAddressDefines::StatisticsType::NONE ); } diff --git a/ApplicationLibCode/FileInterface/RifEclipseSummaryAddressDefines.h b/ApplicationLibCode/FileInterface/RifEclipseSummaryAddressDefines.h index 1016ff4580..03543c3dcc 100644 --- a/ApplicationLibCode/FileInterface/RifEclipseSummaryAddressDefines.h +++ b/ApplicationLibCode/FileInterface/RifEclipseSummaryAddressDefines.h @@ -71,7 +71,8 @@ enum class StatisticsType P10, P50, P90, - MEAN + MEAN, + CUSTOM }; enum class CurveType diff --git a/ApplicationLibCode/ProjectDataModel/RimEnsembleCurveSetInterface.h b/ApplicationLibCode/ProjectDataModel/RimEnsembleCurveSetInterface.h index 9372990dc4..87b3cbf083 100644 --- a/ApplicationLibCode/ProjectDataModel/RimEnsembleCurveSetInterface.h +++ b/ApplicationLibCode/ProjectDataModel/RimEnsembleCurveSetInterface.h @@ -28,8 +28,9 @@ public: virtual void updateAllCurves() = 0; virtual void updateStatisticsCurves() = 0; - virtual bool hasP10Data() const = 0; - virtual bool hasP50Data() const = 0; - virtual bool hasP90Data() const = 0; - virtual bool hasMeanData() const = 0; + virtual bool hasP10Data() const = 0; + virtual bool hasP50Data() const = 0; + virtual bool hasP90Data() const = 0; + virtual bool hasMeanData() const = 0; + virtual bool hasPercentileData( int p ) const = 0; }; diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCrossPlotStatisticsCase.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCrossPlotStatisticsCase.cpp index 3223ab29c5..e0b9f7045f 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCrossPlotStatisticsCase.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCrossPlotStatisticsCase.cpp @@ -280,6 +280,15 @@ bool RimEnsembleCrossPlotStatisticsCase::hasMeanData() const return !m_meanData.empty(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimEnsembleCrossPlotStatisticsCase::hasPercentileData( int /*percentile*/ ) const +{ + // Custom percentiles are not supported for cross-plot statistics + return false; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCrossPlotStatisticsCase.h b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCrossPlotStatisticsCase.h index 348bbbfb99..b5ce0e2ca1 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCrossPlotStatisticsCase.h +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCrossPlotStatisticsCase.h @@ -44,6 +44,7 @@ public: bool hasP50Data() const; bool hasP90Data() const; bool hasMeanData() const; + bool hasPercentileData( int percentile ) const; QString caseName() const override; void createSummaryReaderInterface() override; diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.cpp index d6a29e7d37..3e319b35ce 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.cpp @@ -2231,6 +2231,8 @@ void RimEnsembleCurveSet::updateStatisticsCurves( const std::vectorallSummaryCases(); } + auto percentiles = m_statistics->allPercentiles(); + if ( isXAxisSummaryVector() ) { m_ensembleStatCaseXY->calculate( statCases, @@ -2242,7 +2244,7 @@ void RimEnsembleCurveSet::updateStatisticsCurves( const std::vectorcalculate( statCases, summaryAddressY(), m_statistics->includeIncompleteCurves() ); + m_ensembleStatCaseY->calculate( statCases, summaryAddressY(), m_statistics->includeIncompleteCurves(), percentiles ); } } @@ -2289,6 +2291,16 @@ void RimEnsembleCurveSet::updateStatisticsCurves( const std::vector RiaSummaryCurveAddress + { + auto xStatAddress = RifEclipseSummaryAddress::timeAddress(); + auto yStatAddress = addrY; + yStatAddress.setStatisticsType( RifEclipseSummaryAddressDefines::StatisticsType::CUSTOM ); + yStatAddress.setPercentile( percentile ); + + return RiaSummaryCurveAddress( xStatAddress, yStatAddress ); + }; + if ( m_statistics->showP10Curve() && m_ensembleStatCaseY->hasP10Data() ) addresses.push_back( getStatisticsAddress( RifEclipseSummaryAddressDefines::StatisticsType::P10, dataAddressY ) ); if ( m_statistics->showP50Curve() && m_ensembleStatCaseY->hasP50Data() ) @@ -2297,6 +2309,18 @@ void RimEnsembleCurveSet::updateStatisticsCurves( const std::vectorshowMeanCurve() && m_ensembleStatCaseY->hasMeanData() ) addresses.push_back( getStatisticsAddress( RifEclipseSummaryAddressDefines::StatisticsType::MEAN, dataAddressY ) ); + + // Add custom percentiles (excluding standard P10/P50/P90 if their checkbox is checked) + for ( int p : m_statistics->allPercentiles() ) + { + bool isHandledByCheckbox = ( p == 10 && m_statistics->showP10Curve() ) || ( p == 50 && m_statistics->showP50Curve() ) || + ( p == 90 && m_statistics->showP90Curve() ); + + if ( !isHandledByCheckbox && m_ensembleStatCaseY->hasPercentileData( p ) ) + { + addresses.push_back( getPercentileAddress( p, dataAddressY ) ); + } + } } } @@ -2332,8 +2356,17 @@ void RimEnsembleCurveSet::updateStatisticsCurves( const std::vectorshowCurveLabels() ) { - curve->setSymbolLabel( QString::fromStdString( - RifEclipseSummaryAddressDefines::statisticsTypeToString( address.summaryAddressY().statisticsType() ) ) ); + QString label; + auto yAddr = address.summaryAddressY(); + if ( yAddr.statisticsType() == RifEclipseSummaryAddressDefines::StatisticsType::CUSTOM && yAddr.percentile() >= 0 ) + { + label = QString( "P%1" ).arg( yAddr.percentile() ); + } + else + { + label = QString::fromStdString( RifEclipseSummaryAddressDefines::statisticsTypeToString( yAddr.statisticsType() ) ); + } + curve->setSymbolLabel( label ); } curve->setLineStyle( RiuQwtPlotCurveDefines::LineStyleEnum::STYLE_SOLID ); } @@ -2523,6 +2556,16 @@ bool RimEnsembleCurveSet::hasMeanData() const return m_ensembleStatCaseY->hasMeanData(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimEnsembleCurveSet::hasPercentileData( int p ) const +{ + if ( isXAxisSummaryVector() ) return m_ensembleStatCaseXY->hasPercentileData( p ); + + return m_ensembleStatCaseY->hasPercentileData( p ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.h b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.h index 390bb1b8d6..56a186da36 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.h +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.h @@ -163,6 +163,7 @@ public: bool hasP50Data() const override; bool hasP90Data() const override; bool hasMeanData() const override; + bool hasPercentileData( int p ) const override; const RimEnsembleStatistics* statisticsOptions() const; diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatistics.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatistics.cpp index 37b1c77355..97228f203d 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatistics.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatistics.cpp @@ -19,11 +19,16 @@ #include "RimEnsembleStatistics.h" #include "RiaColorTools.h" +#include "RiaTextStringTools.h" #include "RimEnsembleCurveSet.h" #include "RimEnsembleCurveSetInterface.h" #include "RimProject.h" +#include "cafPdmUiLineEditor.h" + +#include + CAF_PDM_SOURCE_INIT( RimEnsembleStatistics, "RimEnsembleStatistics" ); //-------------------------------------------------------------------------------------------------- @@ -50,6 +55,9 @@ RimEnsembleStatistics::RimEnsembleStatistics( RimEnsembleCurveSetInterface* pare CAF_PDM_InitField( &m_showP50Curve, "ShowP50Curve", false, "P50" ); CAF_PDM_InitField( &m_showP90Curve, "ShowP90Curve", true, "P90" ); CAF_PDM_InitField( &m_showMeanCurve, "ShowMeanCurve", true, "Mean" ); + CAF_PDM_InitField( &m_customPercentiles, "CustomPercentiles", QString(), "Custom Percentiles" ); + m_customPercentiles.uiCapability()->setUiEditorTypeName( caf::PdmUiLineEditor::uiEditorTypeName() ); + 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" ); @@ -149,6 +157,41 @@ bool RimEnsembleStatistics::showMeanCurve() const return m_showMeanCurve; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimEnsembleStatistics::customPercentiles() const +{ + return m_customPercentiles; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimEnsembleStatistics::allPercentiles() const +{ + std::set uniquePercentiles; + + // Add standard percentiles from checkboxes + if ( m_showP10Curve ) uniquePercentiles.insert( 10 ); + if ( m_showP50Curve ) uniquePercentiles.insert( 50 ); + if ( m_showP90Curve ) uniquePercentiles.insert( 90 ); + + // Parse custom percentiles string + QStringList parts = RiaTextStringTools::splitSkipEmptyParts( m_customPercentiles() ); + for ( const auto& part : parts ) + { + bool ok = false; + int value = part.trimmed().toInt( &ok ); + if ( ok && value >= 0 && value <= 100 ) + { + uniquePercentiles.insert( value ); + } + } + + return { uniquePercentiles.begin(), uniquePercentiles.end() }; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -276,6 +319,7 @@ void RimEnsembleStatistics::defaultUiOrdering( bool showCrossPlotGroup, caf::Pdm group->add( &m_showP50Curve ); group->add( &m_showMeanCurve ); group->add( &m_showP10Curve ); + group->add( &m_customPercentiles ); disableP10Curve( !m_active || !curveSet->hasP10Data() ); disableP50Curve( !m_active || !curveSet->hasP50Data() ); @@ -283,6 +327,7 @@ void RimEnsembleStatistics::defaultUiOrdering( bool showCrossPlotGroup, caf::Pdm disableMeanCurve( !m_active || !curveSet->hasMeanData() ); m_showCurveLabels.uiCapability()->setUiReadOnly( !m_active ); m_color.uiCapability()->setUiReadOnly( !m_active ); + m_customPercentiles.uiCapability()->setUiReadOnly( !m_active ); m_showP10Curve.uiCapability()->setUiName( curveSet->hasP10Data() ? "P10" : "P10 (Needs > 8 curves)" ); m_showP90Curve.uiCapability()->setUiName( curveSet->hasP90Data() ? "P90" : "P90 (Needs > 8 curves)" ); @@ -323,6 +368,20 @@ void RimEnsembleStatistics::defineUiOrdering( QString uiConfigName, caf::PdmUiOr defaultUiOrdering( showCrossPlot, uiOrdering ); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimEnsembleStatistics::defineEditorAttribute( const caf::PdmFieldHandle* field, QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) +{ + if ( field == &m_customPercentiles ) + { + if ( auto lineEdAttr = dynamic_cast( attribute ) ) + { + lineEdAttr->placeholderText = "E.g. 5 25 75 95"; + } + } +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -338,3 +397,25 @@ void RimEnsembleStatistics::onSetShowEnsembleCurves( const bool& enable ) { m_hideEnsembleCurves = !enable; } + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimEnsembleStatistics::validateCustomPercentiles() const +{ + QStringList parts = RiaTextStringTools::splitSkipEmptyParts( m_customPercentiles() ); + for ( const auto& part : parts ) + { + bool ok = false; + int value = part.trimmed().toInt( &ok ); + if ( !ok ) + { + return QString( "Invalid percentile value: '%1'" ).arg( part.trimmed() ); + } + if ( value < 0 || value > 100 ) + { + return QString( "Percentile value %1 must be in the range 0 to 100" ).arg( value ); + } + } + return {}; +} diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatistics.h b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatistics.h index e98797228f..a1dd7940e6 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatistics.h +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatistics.h @@ -23,6 +23,8 @@ #include "cafPdmObject.h" #include "cafPdmProxyValueField.h" +#include + class RimEnsembleCurveSetInterface; //================================================================================================== @@ -45,6 +47,9 @@ public: bool showP90Curve() const; bool showMeanCurve() const; + QString customPercentiles() const; + std::vector allPercentiles() const; + bool showCurveLabels() const; void enableCurveLabels( bool enable ); @@ -67,10 +72,12 @@ public: void defaultUiOrdering( bool showCrossPlotGroup, caf::PdmUiOrdering& uiOrdering ); private: - void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override; - void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; - bool onShowEnsembleCurves() const; - void onSetShowEnsembleCurves( const bool& enable ); + void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override; + void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; + void defineEditorAttribute( const caf::PdmFieldHandle* field, QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) override; + bool onShowEnsembleCurves() const; + void onSetShowEnsembleCurves( const bool& enable ); + QString validateCustomPercentiles() const; private: RimEnsembleCurveSetInterface* m_parentCurveSet; @@ -84,6 +91,7 @@ private: caf::PdmField m_showP50Curve; caf::PdmField m_showP90Curve; caf::PdmField m_showMeanCurve; + caf::PdmField m_customPercentiles; caf::PdmField m_showCurveLabels; caf::PdmField m_includeIncompleteCurves; caf::PdmField m_customColor; diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatisticsCase.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatisticsCase.cpp index 2e698c7661..47cc246490 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatisticsCase.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatisticsCase.cpp @@ -24,8 +24,11 @@ #include "RiaTimeHistoryCurveResampler.h" #include "Summary/RiaSummaryTools.h" +#include "RifEclipseSummaryAddress.h" + #include "RigStatisticsMath.h" +#include #include //-------------------------------------------------------------------------------------------------- @@ -41,7 +44,7 @@ std::vector RimEnsembleStatisticsCase::timeSteps( const RifEclipseSummar //-------------------------------------------------------------------------------------------------- bool RimEnsembleStatisticsCase::hasP10Data() const { - return !m_p10Data.empty(); + return hasPercentileData( 10 ); } //-------------------------------------------------------------------------------------------------- @@ -49,7 +52,7 @@ bool RimEnsembleStatisticsCase::hasP10Data() const //-------------------------------------------------------------------------------------------------- bool RimEnsembleStatisticsCase::hasP50Data() const { - return !m_p50Data.empty(); + return hasPercentileData( 50 ); } //-------------------------------------------------------------------------------------------------- @@ -57,7 +60,7 @@ bool RimEnsembleStatisticsCase::hasP50Data() const //-------------------------------------------------------------------------------------------------- bool RimEnsembleStatisticsCase::hasP90Data() const { - return !m_p90Data.empty(); + return hasPercentileData( 90 ); } //-------------------------------------------------------------------------------------------------- @@ -68,6 +71,17 @@ bool RimEnsembleStatisticsCase::hasMeanData() const return !m_meanData.empty(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimEnsembleStatisticsCase::hasPercentileData( int percentile ) const +{ + if ( percentile < RifEclipseSummaryAddress::MIN_PERCENTILE || percentile > RifEclipseSummaryAddress::MAX_PERCENTILE ) return false; + + auto it = m_percentileData.find( percentile ); + return it != m_percentileData.end() && !it->second.empty(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -75,19 +89,40 @@ std::pair> RimEnsembleStatisticsCase::values( const Ri { if ( resultAddress.isErrorResult() ) return { true, {} }; + if ( resultAddress.statisticsType() == RifEclipseSummaryAddressDefines::StatisticsType::MEAN ) + { + return { true, m_meanData }; + } + + int percentile = -1; switch ( resultAddress.statisticsType() ) { case RifEclipseSummaryAddressDefines::StatisticsType::P10: - return { true, m_p10Data }; + percentile = 10; + break; case RifEclipseSummaryAddressDefines::StatisticsType::P50: - return { true, m_p50Data }; + percentile = 50; + break; case RifEclipseSummaryAddressDefines::StatisticsType::P90: - return { true, m_p90Data }; - case RifEclipseSummaryAddressDefines::StatisticsType::MEAN: - return { true, m_meanData }; + percentile = 90; + break; + case RifEclipseSummaryAddressDefines::StatisticsType::CUSTOM: + percentile = resultAddress.percentile(); + break; default: return { true, {} }; } + + if ( percentile >= RifEclipseSummaryAddress::MIN_PERCENTILE && percentile <= RifEclipseSummaryAddress::MAX_PERCENTILE ) + { + auto it = m_percentileData.find( percentile ); + if ( it != m_percentileData.end() ) + { + return { true, it->second }; + } + } + + return { true, {} }; } //-------------------------------------------------------------------------------------------------- @@ -132,9 +167,10 @@ RifSummaryReaderInterface* RimEnsembleStatisticsCase::summaryReader() //-------------------------------------------------------------------------------------------------- void RimEnsembleStatisticsCase::calculate( const std::vector& summaryCases, const RifEclipseSummaryAddress& inputAddress, - bool includeIncompleteCurves ) + bool includeIncompleteCurves, + const std::vector& percentiles ) { - auto hash = RiaHashTools::hash( summaryCases, inputAddress.toEclipseTextAddress(), includeIncompleteCurves ); + auto hash = RiaHashTools::hash( summaryCases, inputAddress.toEclipseTextAddress(), includeIncompleteCurves, percentiles ); if ( hash == m_hash ) return; auto startTime = RiaLogging::currentTime(); @@ -143,6 +179,18 @@ void RimEnsembleStatisticsCase::calculate( const std::vector& s clearData(); + m_requestedPercentiles = percentiles; + + // Always include P10, P50, and P90 + for ( int p : { 10, 50, 90 } ) + { + if ( std::find( m_requestedPercentiles.begin(), m_requestedPercentiles.end(), p ) == m_requestedPercentiles.end() ) + { + m_requestedPercentiles.push_back( p ); + } + } + std::sort( m_requestedPercentiles.begin(), m_requestedPercentiles.end() ); + if ( !inputAddress.isValid() ) return; if ( summaryCases.empty() ) return; @@ -200,11 +248,14 @@ void RimEnsembleStatisticsCase::calculate( const std::vector& s m_timeSteps = curveMerger.allXValues(); - m_p10Data.reserve( m_timeSteps.size() ); - m_p50Data.reserve( m_timeSteps.size() ); - m_p90Data.reserve( m_timeSteps.size() ); m_meanData.reserve( m_timeSteps.size() ); + // Initialize percentile storage + for ( int p : m_requestedPercentiles ) + { + m_percentileData[p].reserve( m_timeSteps.size() ); + } + for ( size_t timeStepIndex = 0; timeStepIndex < m_timeSteps.size(); timeStepIndex++ ) { std::vector valuesAtTimeStep; @@ -215,12 +266,27 @@ void RimEnsembleStatisticsCase::calculate( const std::vector& s valuesAtTimeStep.push_back( curveValues[curveIdx][timeStepIndex] ); } - double p10, p50, p90, mean; - RigStatisticsMath::calculateStatisticsCurves( valuesAtTimeStep, &p10, &p50, &p90, &mean, RigStatisticsMath::PercentileStyle::SWITCHED ); - m_p10Data.push_back( p10 ); - m_p50Data.push_back( p50 ); - m_p90Data.push_back( p90 ); + double mean = RigStatisticsMath::calculateMean( valuesAtTimeStep ); m_meanData.push_back( mean ); + + // Calculate percentiles + std::vector percentilePositions; + for ( int p : m_requestedPercentiles ) + { + percentilePositions.push_back( static_cast( p ) / 100.0 ); + } + + auto percentileValuesResult = + RigStatisticsMath::calculatePercentiles( valuesAtTimeStep, percentilePositions, RigStatisticsMath::PercentileStyle::SWITCHED ); + + if ( percentileValuesResult.has_value() ) + { + const auto& percentileValues = *percentileValuesResult; + for ( size_t i = 0; i < m_requestedPercentiles.size(); i++ ) + { + m_percentileData[m_requestedPercentiles[i]].push_back( percentileValues[i] ); + } + } } bool showDebugTiming = false; @@ -250,10 +316,9 @@ RiaDefines::EclipseUnitSystem RimEnsembleStatisticsCase::unitSystem() const void RimEnsembleStatisticsCase::clearData() { m_timeSteps.clear(); - m_p10Data.clear(); - m_p50Data.clear(); - m_p90Data.clear(); m_meanData.clear(); + m_percentileData.clear(); + m_requestedPercentiles.clear(); m_firstSummaryCase = nullptr; } diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatisticsCase.h b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatisticsCase.h index 2606f2c606..aeacddf2ee 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatisticsCase.h +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleStatisticsCase.h @@ -25,6 +25,8 @@ #include "RimSummaryCase.h" +#include + class RifEclipseSummaryAddress; //================================================================================================== @@ -37,13 +39,17 @@ public: bool hasP50Data() const; bool hasP90Data() const; bool hasMeanData() const; + bool hasPercentileData( int percentile ) const; QString caseName() const override; void createSummaryReaderInterface() override; RifSummaryReaderInterface* summaryReader() override; RiaDefines::EclipseUnitSystem unitSystem() const override; - void calculate( const std::vector& summaryCases, const RifEclipseSummaryAddress& inputAddress, bool includeIncompleteCurves ); + void calculate( const std::vector& summaryCases, + const RifEclipseSummaryAddress& inputAddress, + bool includeIncompleteCurves, + const std::vector& percentiles = { 10, 50, 90 } ); std::vector timeSteps( const RifEclipseSummaryAddress& resultAddress ) const override; std::pair> values( const RifEclipseSummaryAddress& resultAddress ) const override; @@ -58,11 +64,10 @@ private: size_t keywordCount() const override; private: - std::vector m_timeSteps; - std::vector m_p10Data; - std::vector m_p50Data; - std::vector m_p90Data; - std::vector m_meanData; + std::vector m_timeSteps; + std::vector m_meanData; + std::map> m_percentileData; + std::vector m_requestedPercentiles; caf::PdmPointer m_firstSummaryCase; size_t m_hash = 0; diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryAddress.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryAddress.cpp index 489fa4590b..94e8543b81 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryAddress.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryAddress.cpp @@ -80,6 +80,7 @@ RimSummaryAddress::RimSummaryAddress() CAF_PDM_InitField( &m_caseId, "CaseId", -1, "CaseId" ); CAF_PDM_InitField( &m_ensembleId, "EnsembleId", -1, "EnsembleId" ); + CAF_PDM_InitField( &m_percentile, "Percentile", -1, "Percentile" ); m_category = RifEclipseSummaryAddressDefines::SummaryCategory::SUMMARY_INVALID; m_statistics = RifEclipseSummaryAddressDefines::StatisticsType::NONE; @@ -138,6 +139,7 @@ void RimSummaryAddress::setAddress( const RifEclipseSummaryAddress& addr ) m_cellJ = addr.cellJ(); m_cellK = addr.cellK(); m_calculationId = addr.id(); + m_percentile = addr.percentile(); setUiName( m_vectorName ); setUiIconFromResourceString( iconResourceText() ); @@ -148,23 +150,25 @@ void RimSummaryAddress::setAddress( const RifEclipseSummaryAddress& addr ) //-------------------------------------------------------------------------------------------------- RifEclipseSummaryAddress RimSummaryAddress::address() const { - return RifEclipseSummaryAddress( m_category(), - m_statistics(), - m_vectorName().toStdString(), - m_regionNumber(), - m_regionNumber2(), - m_groupName().toStdString(), - m_networkName().toStdString(), - m_wellName().toStdString(), - m_wellSegmentNumber(), - m_lgrName().toStdString(), - m_cellI(), - m_cellJ(), - m_cellK(), - m_aquiferNumber, - m_wellCompletionNumber(), - m_isErrorResult, - m_calculationId ); + auto addr = RifEclipseSummaryAddress( m_category(), + m_statistics(), + m_vectorName().toStdString(), + m_regionNumber(), + m_regionNumber2(), + m_groupName().toStdString(), + m_networkName().toStdString(), + m_wellName().toStdString(), + m_wellSegmentNumber(), + m_lgrName().toStdString(), + m_cellI(), + m_cellJ(), + m_cellK(), + m_aquiferNumber, + m_wellCompletionNumber(), + m_isErrorResult, + m_calculationId ); + addr.setPercentile( m_percentile ); + return addr; } //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryAddress.h b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryAddress.h index b89527e80c..c687625cdd 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryAddress.h +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryAddress.h @@ -85,4 +85,5 @@ private: caf::PdmField m_calculationId; caf::PdmField m_caseId; caf::PdmField m_ensembleId; + caf::PdmField m_percentile; }; diff --git a/ApplicationLibCode/ProjectDataModel/WellLog/RimEnsembleWellLogCurveSet.cpp b/ApplicationLibCode/ProjectDataModel/WellLog/RimEnsembleWellLogCurveSet.cpp index af5bb28b45..991662e62e 100644 --- a/ApplicationLibCode/ProjectDataModel/WellLog/RimEnsembleWellLogCurveSet.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellLog/RimEnsembleWellLogCurveSet.cpp @@ -1088,6 +1088,15 @@ bool RimEnsembleWellLogCurveSet::hasMeanData() const return m_ensembleWellLogStatistics->hasMeanData(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimEnsembleWellLogCurveSet::hasPercentileData( int /*p*/ ) const +{ + // Custom percentiles are not supported for well log statistics + return false; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/WellLog/RimEnsembleWellLogCurveSet.h b/ApplicationLibCode/ProjectDataModel/WellLog/RimEnsembleWellLogCurveSet.h index 8db3fe3df5..2301694c41 100644 --- a/ApplicationLibCode/ProjectDataModel/WellLog/RimEnsembleWellLogCurveSet.h +++ b/ApplicationLibCode/ProjectDataModel/WellLog/RimEnsembleWellLogCurveSet.h @@ -108,6 +108,7 @@ public: bool hasP50Data() const override; bool hasP90Data() const override; bool hasMeanData() const override; + bool hasPercentileData( int p ) const override; void appendColorGroup( caf::PdmUiOrdering& uiOrdering );