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
This commit is contained in:
Magne Sjaastad committed 2026-02-12 14:25:48 +01:00
1 parent 8f365ea340
commit a2b3445692
18 files changed
+350 -58

No files matched your search

@@ -21,6 +21,8 @@
#include <cstddef> #include <cstddef>
#include <functional> #include <functional>
#include <ranges> #include <ranges>
#include <string>
#include <type_traits>
//================================================================================================== //==================================================================================================
// //
@@ -31,14 +33,29 @@ namespace RiaHashTools
{ {
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
/// Variadic template function to combine multiple parameters into a single hash /// 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 <typename T> template <typename T>
requires( !std::ranges::range<T> || std::is_same_v<std::decay_t<T>, std::string> )
void combineHash( size_t& seed, const T& value ) 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 // Based on https://www.boost.org/doc/libs/1_84_0/libs/container_hash/doc/html/hash.html#notes_hash_combine
seed ^= std::hash<T>()( value ) + 0x9e3779b9 + ( seed << 6 ) + ( seed >> 2 ); seed ^= std::hash<T>()( value ) + 0x9e3779b9 + ( seed << 6 ) + ( seed >> 2 );
} }
//--------------------------------------------------------------------------------------------------
/// Overload for range types (excluding std::string)
//--------------------------------------------------------------------------------------------------
template <std::ranges::range Range>
requires( !std::is_same_v<std::decay_t<Range>, std::string> )
void combineHash( size_t& seed, const Range& range )
{
for ( const auto& elem : range )
{
combineHash( seed, elem );
}
}
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
/// ///
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
@@ -44,6 +44,7 @@ RifEclipseSummaryAddress::RifEclipseSummaryAddress( SummaryCategory category, st
, m_number2( -1 ) , m_number2( -1 )
, m_isErrorResult( false ) , m_isErrorResult( false )
, m_id( -1 ) , m_id( -1 )
, m_percentile( -1 )
{ {
std::pair<int, int> reg2regPair; std::pair<int, int> reg2regPair;
switch ( category ) switch ( category )
@@ -132,6 +133,7 @@ RifEclipseSummaryAddress::RifEclipseSummaryAddress( SummaryCategory category,
, m_number2( -1 ) , m_number2( -1 )
, m_isErrorResult( isErrorResult ) , m_isErrorResult( isErrorResult )
, m_id( id ) , m_id( id )
, m_percentile( -1 )
{ {
switch ( category ) switch ( category )
{ {
@@ -196,6 +198,7 @@ RifEclipseSummaryAddress::RifEclipseSummaryAddress()
, m_number2( -1 ) , m_number2( -1 )
, m_isErrorResult( false ) , m_isErrorResult( false )
, m_id( -1 ) , m_id( -1 )
, m_percentile( -1 )
{ {
} }
@@ -669,8 +672,17 @@ std::string RifEclipseSummaryAddress::uiText() const
if ( isStatistics() ) if ( isStatistics() )
{ {
auto prefix = RifEclipseSummaryAddressDefines::statisticsTypeToString( statisticsType() ); std::string prefix;
text = prefix + ":" + text; 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; return text;
@@ -1027,6 +1039,31 @@ void RifEclipseSummaryAddress::setId( int id )
m_id = 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;
}
}
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
/// ///
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
@@ -110,6 +110,8 @@ public:
bool isStatistics() const; bool isStatistics() const;
StatisticsType statisticsType() const; StatisticsType statisticsType() const;
void setStatisticsType( StatisticsType type ); void setStatisticsType( StatisticsType type );
int percentile() const;
void setPercentile( int percentile );
int regionNumber() const; int regionNumber() const;
int regionNumber2() const; int regionNumber2() const;
@@ -166,6 +168,10 @@ public:
std::string formatUiTextRegionToRegion() const; std::string formatUiTextRegionToRegion() const;
static std::pair<int, int> regionToRegionPairFromUiText( const std::string& s ); static std::pair<int, int> regionToRegionPairFromUiText( const std::string& s );
// Percentile constants
static constexpr int MIN_PERCENTILE = 0;
static constexpr int MAX_PERCENTILE = 100;
private: private:
static RifEclipseSummaryAddress fromTokens( const std::vector<std::string>& tokens ); static RifEclipseSummaryAddress fromTokens( const std::vector<std::string>& tokens );
@@ -188,6 +194,7 @@ private:
int m_number2; int m_number2;
bool m_isErrorResult; bool m_isErrorResult;
int m_id; int m_id;
int m_percentile;
}; };
QTextStream& operator<<( QTextStream& str, const RifEclipseSummaryAddress& sobj ); QTextStream& operator<<( QTextStream& str, const RifEclipseSummaryAddress& sobj );
@@ -30,6 +30,7 @@ void caf::AppEnum<RifEclipseSummaryAddressDefines::StatisticsType>::setUp()
addItem( RifEclipseSummaryAddressDefines::StatisticsType::P50, "P50", "P50" ); addItem( RifEclipseSummaryAddressDefines::StatisticsType::P50, "P50", "P50" );
addItem( RifEclipseSummaryAddressDefines::StatisticsType::P90, "P90", "P90" ); addItem( RifEclipseSummaryAddressDefines::StatisticsType::P90, "P90", "P90" );
addItem( RifEclipseSummaryAddressDefines::StatisticsType::MEAN, "MEAN", "Mean" ); addItem( RifEclipseSummaryAddressDefines::StatisticsType::MEAN, "MEAN", "Mean" );
addItem( RifEclipseSummaryAddressDefines::StatisticsType::CUSTOM, "CUSTOM", "Custom" );
setDefault( RifEclipseSummaryAddressDefines::StatisticsType::NONE ); setDefault( RifEclipseSummaryAddressDefines::StatisticsType::NONE );
} }
@@ -71,7 +71,8 @@ enum class StatisticsType
P10, P10,
P50, P50,
P90, P90,
MEAN MEAN,
CUSTOM
}; };
enum class CurveType enum class CurveType
@@ -28,8 +28,9 @@ public:
virtual void updateAllCurves() = 0; virtual void updateAllCurves() = 0;
virtual void updateStatisticsCurves() = 0; virtual void updateStatisticsCurves() = 0;
virtual bool hasP10Data() const = 0; virtual bool hasP10Data() const = 0;
virtual bool hasP50Data() const = 0; virtual bool hasP50Data() const = 0;
virtual bool hasP90Data() const = 0; virtual bool hasP90Data() const = 0;
virtual bool hasMeanData() const = 0; virtual bool hasMeanData() const = 0;
virtual bool hasPercentileData( int p ) const = 0;
}; };
@@ -280,6 +280,15 @@ bool RimEnsembleCrossPlotStatisticsCase::hasMeanData() const
return !m_meanData.empty(); return !m_meanData.empty();
} }
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RimEnsembleCrossPlotStatisticsCase::hasPercentileData( int /*percentile*/ ) const
{
// Custom percentiles are not supported for cross-plot statistics
return false;
}
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
/// ///
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
@@ -44,6 +44,7 @@ public:
bool hasP50Data() const; bool hasP50Data() const;
bool hasP90Data() const; bool hasP90Data() const;
bool hasMeanData() const; bool hasMeanData() const;
bool hasPercentileData( int percentile ) const;
QString caseName() const override; QString caseName() const override;
void createSummaryReaderInterface() override; void createSummaryReaderInterface() override;
@@ -2231,6 +2231,8 @@ void RimEnsembleCurveSet::updateStatisticsCurves( const std::vector<RimSummaryCa
statCases = group->allSummaryCases(); statCases = group->allSummaryCases();
} }
auto percentiles = m_statistics->allPercentiles();
if ( isXAxisSummaryVector() ) if ( isXAxisSummaryVector() )
{ {
m_ensembleStatCaseXY->calculate( statCases, m_ensembleStatCaseXY->calculate( statCases,
@@ -2242,7 +2244,7 @@ void RimEnsembleCurveSet::updateStatisticsCurves( const std::vector<RimSummaryCa
} }
else else
{ {
m_ensembleStatCaseY->calculate( statCases, summaryAddressY(), m_statistics->includeIncompleteCurves() ); m_ensembleStatCaseY->calculate( statCases, summaryAddressY(), m_statistics->includeIncompleteCurves(), percentiles );
} }
} }
@@ -2289,6 +2291,16 @@ void RimEnsembleCurveSet::updateStatisticsCurves( const std::vector<RimSummaryCa
return RiaSummaryCurveAddress( xStatAddress, yStatAddress ); return RiaSummaryCurveAddress( xStatAddress, yStatAddress );
}; };
auto getPercentileAddress = []( int percentile, const RifEclipseSummaryAddress& addrY ) -> 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() ) if ( m_statistics->showP10Curve() && m_ensembleStatCaseY->hasP10Data() )
addresses.push_back( getStatisticsAddress( RifEclipseSummaryAddressDefines::StatisticsType::P10, dataAddressY ) ); addresses.push_back( getStatisticsAddress( RifEclipseSummaryAddressDefines::StatisticsType::P10, dataAddressY ) );
if ( m_statistics->showP50Curve() && m_ensembleStatCaseY->hasP50Data() ) if ( m_statistics->showP50Curve() && m_ensembleStatCaseY->hasP50Data() )
@@ -2297,6 +2309,18 @@ void RimEnsembleCurveSet::updateStatisticsCurves( const std::vector<RimSummaryCa
addresses.push_back( getStatisticsAddress( RifEclipseSummaryAddressDefines::StatisticsType::P90, dataAddressY ) ); addresses.push_back( getStatisticsAddress( RifEclipseSummaryAddressDefines::StatisticsType::P90, dataAddressY ) );
if ( m_statistics->showMeanCurve() && m_ensembleStatCaseY->hasMeanData() ) if ( m_statistics->showMeanCurve() && m_ensembleStatCaseY->hasMeanData() )
addresses.push_back( getStatisticsAddress( RifEclipseSummaryAddressDefines::StatisticsType::MEAN, dataAddressY ) ); 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::vector<RimSummaryCa
if ( m_statistics->showCurveLabels() ) if ( m_statistics->showCurveLabels() )
{ {
curve->setSymbolLabel( QString::fromStdString( QString label;
RifEclipseSummaryAddressDefines::statisticsTypeToString( address.summaryAddressY().statisticsType() ) ) ); 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 ); curve->setLineStyle( RiuQwtPlotCurveDefines::LineStyleEnum::STYLE_SOLID );
} }
@@ -2523,6 +2556,16 @@ bool RimEnsembleCurveSet::hasMeanData() const
return m_ensembleStatCaseY->hasMeanData(); return m_ensembleStatCaseY->hasMeanData();
} }
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RimEnsembleCurveSet::hasPercentileData( int p ) const
{
if ( isXAxisSummaryVector() ) return m_ensembleStatCaseXY->hasPercentileData( p );
return m_ensembleStatCaseY->hasPercentileData( p );
}
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
/// ///
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
@@ -163,6 +163,7 @@ public:
bool hasP50Data() const override; bool hasP50Data() const override;
bool hasP90Data() const override; bool hasP90Data() const override;
bool hasMeanData() const override; bool hasMeanData() const override;
bool hasPercentileData( int p ) const override;
const RimEnsembleStatistics* statisticsOptions() const; const RimEnsembleStatistics* statisticsOptions() const;
@@ -19,11 +19,16 @@
#include "RimEnsembleStatistics.h" #include "RimEnsembleStatistics.h"
#include "RiaColorTools.h" #include "RiaColorTools.h"
#include "RiaTextStringTools.h"
#include "RimEnsembleCurveSet.h" #include "RimEnsembleCurveSet.h"
#include "RimEnsembleCurveSetInterface.h" #include "RimEnsembleCurveSetInterface.h"
#include "RimProject.h" #include "RimProject.h"
#include "cafPdmUiLineEditor.h"
#include <algorithm>
CAF_PDM_SOURCE_INIT( RimEnsembleStatistics, "RimEnsembleStatistics" ); 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_showP50Curve, "ShowP50Curve", false, "P50" );
CAF_PDM_InitField( &m_showP90Curve, "ShowP90Curve", true, "P90" ); CAF_PDM_InitField( &m_showP90Curve, "ShowP90Curve", true, "P90" );
CAF_PDM_InitField( &m_showMeanCurve, "ShowMeanCurve", true, "Mean" ); 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_showCurveLabels, "ShowCurveLabels", true, "Show Curve Labels" );
CAF_PDM_InitField( &m_includeIncompleteCurves, "IncludeIncompleteCurves", false, "Include Incomplete Curves" ); CAF_PDM_InitField( &m_includeIncompleteCurves, "IncludeIncompleteCurves", false, "Include Incomplete Curves" );
@@ -149,6 +157,41 @@ bool RimEnsembleStatistics::showMeanCurve() const
return m_showMeanCurve; return m_showMeanCurve;
} }
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimEnsembleStatistics::customPercentiles() const
{
return m_customPercentiles;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<int> RimEnsembleStatistics::allPercentiles() const
{
std::set<int> 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_showP50Curve );
group->add( &m_showMeanCurve ); group->add( &m_showMeanCurve );
group->add( &m_showP10Curve ); group->add( &m_showP10Curve );
group->add( &m_customPercentiles );
disableP10Curve( !m_active || !curveSet->hasP10Data() ); disableP10Curve( !m_active || !curveSet->hasP10Data() );
disableP50Curve( !m_active || !curveSet->hasP50Data() ); disableP50Curve( !m_active || !curveSet->hasP50Data() );
@@ -283,6 +327,7 @@ void RimEnsembleStatistics::defaultUiOrdering( bool showCrossPlotGroup, caf::Pdm
disableMeanCurve( !m_active || !curveSet->hasMeanData() ); disableMeanCurve( !m_active || !curveSet->hasMeanData() );
m_showCurveLabels.uiCapability()->setUiReadOnly( !m_active ); m_showCurveLabels.uiCapability()->setUiReadOnly( !m_active );
m_color.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_showP10Curve.uiCapability()->setUiName( curveSet->hasP10Data() ? "P10" : "P10 (Needs > 8 curves)" );
m_showP90Curve.uiCapability()->setUiName( curveSet->hasP90Data() ? "P90" : "P90 (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 ); defaultUiOrdering( showCrossPlot, uiOrdering );
} }
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimEnsembleStatistics::defineEditorAttribute( const caf::PdmFieldHandle* field, QString uiConfigName, caf::PdmUiEditorAttribute* attribute )
{
if ( field == &m_customPercentiles )
{
if ( auto lineEdAttr = dynamic_cast<caf::PdmUiLineEditorAttribute*>( attribute ) )
{
lineEdAttr->placeholderText = "E.g. 5 25 75 95";
}
}
}
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
/// ///
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
@@ -338,3 +397,25 @@ void RimEnsembleStatistics::onSetShowEnsembleCurves( const bool& enable )
{ {
m_hideEnsembleCurves = !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 {};
}
@@ -23,6 +23,8 @@
#include "cafPdmObject.h" #include "cafPdmObject.h"
#include "cafPdmProxyValueField.h" #include "cafPdmProxyValueField.h"
#include <vector>
class RimEnsembleCurveSetInterface; class RimEnsembleCurveSetInterface;
//================================================================================================== //==================================================================================================
@@ -45,6 +47,9 @@ public:
bool showP90Curve() const; bool showP90Curve() const;
bool showMeanCurve() const; bool showMeanCurve() const;
QString customPercentiles() const;
std::vector<int> allPercentiles() const;
bool showCurveLabels() const; bool showCurveLabels() const;
void enableCurveLabels( bool enable ); void enableCurveLabels( bool enable );
@@ -67,10 +72,12 @@ public:
void defaultUiOrdering( bool showCrossPlotGroup, caf::PdmUiOrdering& uiOrdering ); void defaultUiOrdering( bool showCrossPlotGroup, caf::PdmUiOrdering& uiOrdering );
private: private:
void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override; void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override;
void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override;
bool onShowEnsembleCurves() const; void defineEditorAttribute( const caf::PdmFieldHandle* field, QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) override;
void onSetShowEnsembleCurves( const bool& enable ); bool onShowEnsembleCurves() const;
void onSetShowEnsembleCurves( const bool& enable );
QString validateCustomPercentiles() const;
private: private:
RimEnsembleCurveSetInterface* m_parentCurveSet; RimEnsembleCurveSetInterface* m_parentCurveSet;
@@ -84,6 +91,7 @@ private:
caf::PdmField<bool> m_showP50Curve; caf::PdmField<bool> m_showP50Curve;
caf::PdmField<bool> m_showP90Curve; caf::PdmField<bool> m_showP90Curve;
caf::PdmField<bool> m_showMeanCurve; caf::PdmField<bool> m_showMeanCurve;
caf::PdmField<QString> m_customPercentiles;
caf::PdmField<bool> m_showCurveLabels; caf::PdmField<bool> m_showCurveLabels;
caf::PdmField<bool> m_includeIncompleteCurves; caf::PdmField<bool> m_includeIncompleteCurves;
caf::PdmField<bool> m_customColor; caf::PdmField<bool> m_customColor;
@@ -24,8 +24,11 @@
#include "RiaTimeHistoryCurveResampler.h" #include "RiaTimeHistoryCurveResampler.h"
#include "Summary/RiaSummaryTools.h" #include "Summary/RiaSummaryTools.h"
#include "RifEclipseSummaryAddress.h"
#include "RigStatisticsMath.h" #include "RigStatisticsMath.h"
#include <algorithm>
#include <limits> #include <limits>
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
@@ -41,7 +44,7 @@ std::vector<time_t> RimEnsembleStatisticsCase::timeSteps( const RifEclipseSummar
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
bool RimEnsembleStatisticsCase::hasP10Data() const bool RimEnsembleStatisticsCase::hasP10Data() const
{ {
return !m_p10Data.empty(); return hasPercentileData( 10 );
} }
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
@@ -49,7 +52,7 @@ bool RimEnsembleStatisticsCase::hasP10Data() const
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
bool RimEnsembleStatisticsCase::hasP50Data() const bool RimEnsembleStatisticsCase::hasP50Data() const
{ {
return !m_p50Data.empty(); return hasPercentileData( 50 );
} }
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
@@ -57,7 +60,7 @@ bool RimEnsembleStatisticsCase::hasP50Data() const
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
bool RimEnsembleStatisticsCase::hasP90Data() const bool RimEnsembleStatisticsCase::hasP90Data() const
{ {
return !m_p90Data.empty(); return hasPercentileData( 90 );
} }
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
@@ -68,6 +71,17 @@ bool RimEnsembleStatisticsCase::hasMeanData() const
return !m_meanData.empty(); 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<bool, std::vector<double>> RimEnsembleStatisticsCase::values( const Ri
{ {
if ( resultAddress.isErrorResult() ) return { true, {} }; if ( resultAddress.isErrorResult() ) return { true, {} };
if ( resultAddress.statisticsType() == RifEclipseSummaryAddressDefines::StatisticsType::MEAN )
{
return { true, m_meanData };
}
int percentile = -1;
switch ( resultAddress.statisticsType() ) switch ( resultAddress.statisticsType() )
{ {
case RifEclipseSummaryAddressDefines::StatisticsType::P10: case RifEclipseSummaryAddressDefines::StatisticsType::P10:
return { true, m_p10Data }; percentile = 10;
break;
case RifEclipseSummaryAddressDefines::StatisticsType::P50: case RifEclipseSummaryAddressDefines::StatisticsType::P50:
return { true, m_p50Data }; percentile = 50;
break;
case RifEclipseSummaryAddressDefines::StatisticsType::P90: case RifEclipseSummaryAddressDefines::StatisticsType::P90:
return { true, m_p90Data }; percentile = 90;
case RifEclipseSummaryAddressDefines::StatisticsType::MEAN: break;
return { true, m_meanData }; case RifEclipseSummaryAddressDefines::StatisticsType::CUSTOM:
percentile = resultAddress.percentile();
break;
default: default:
return { true, {} }; 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<RimSummaryCase*>& summaryCases, void RimEnsembleStatisticsCase::calculate( const std::vector<RimSummaryCase*>& summaryCases,
const RifEclipseSummaryAddress& inputAddress, const RifEclipseSummaryAddress& inputAddress,
bool includeIncompleteCurves ) bool includeIncompleteCurves,
const std::vector<int>& percentiles )
{ {
auto hash = RiaHashTools::hash( summaryCases, inputAddress.toEclipseTextAddress(), includeIncompleteCurves ); auto hash = RiaHashTools::hash( summaryCases, inputAddress.toEclipseTextAddress(), includeIncompleteCurves, percentiles );
if ( hash == m_hash ) return; if ( hash == m_hash ) return;
auto startTime = RiaLogging::currentTime(); auto startTime = RiaLogging::currentTime();
@@ -143,6 +179,18 @@ void RimEnsembleStatisticsCase::calculate( const std::vector<RimSummaryCase*>& s
clearData(); 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 ( !inputAddress.isValid() ) return;
if ( summaryCases.empty() ) return; if ( summaryCases.empty() ) return;
@@ -200,11 +248,14 @@ void RimEnsembleStatisticsCase::calculate( const std::vector<RimSummaryCase*>& s
m_timeSteps = curveMerger.allXValues(); 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() ); 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++ ) for ( size_t timeStepIndex = 0; timeStepIndex < m_timeSteps.size(); timeStepIndex++ )
{ {
std::vector<double> valuesAtTimeStep; std::vector<double> valuesAtTimeStep;
@@ -215,12 +266,27 @@ void RimEnsembleStatisticsCase::calculate( const std::vector<RimSummaryCase*>& s
valuesAtTimeStep.push_back( curveValues[curveIdx][timeStepIndex] ); valuesAtTimeStep.push_back( curveValues[curveIdx][timeStepIndex] );
} }
double p10, p50, p90, mean; double mean = RigStatisticsMath::calculateMean( valuesAtTimeStep );
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 );
m_meanData.push_back( mean ); m_meanData.push_back( mean );
// Calculate percentiles
std::vector<double> percentilePositions;
for ( int p : m_requestedPercentiles )
{
percentilePositions.push_back( static_cast<double>( 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; bool showDebugTiming = false;
@@ -250,10 +316,9 @@ RiaDefines::EclipseUnitSystem RimEnsembleStatisticsCase::unitSystem() const
void RimEnsembleStatisticsCase::clearData() void RimEnsembleStatisticsCase::clearData()
{ {
m_timeSteps.clear(); m_timeSteps.clear();
m_p10Data.clear();
m_p50Data.clear();
m_p90Data.clear();
m_meanData.clear(); m_meanData.clear();
m_percentileData.clear();
m_requestedPercentiles.clear();
m_firstSummaryCase = nullptr; m_firstSummaryCase = nullptr;
} }
@@ -25,6 +25,8 @@
#include "RimSummaryCase.h" #include "RimSummaryCase.h"
#include <map>
class RifEclipseSummaryAddress; class RifEclipseSummaryAddress;
//================================================================================================== //==================================================================================================
@@ -37,13 +39,17 @@ public:
bool hasP50Data() const; bool hasP50Data() const;
bool hasP90Data() const; bool hasP90Data() const;
bool hasMeanData() const; bool hasMeanData() const;
bool hasPercentileData( int percentile ) const;
QString caseName() const override; QString caseName() const override;
void createSummaryReaderInterface() override; void createSummaryReaderInterface() override;
RifSummaryReaderInterface* summaryReader() override; RifSummaryReaderInterface* summaryReader() override;
RiaDefines::EclipseUnitSystem unitSystem() const override; RiaDefines::EclipseUnitSystem unitSystem() const override;
void calculate( const std::vector<RimSummaryCase*>& summaryCases, const RifEclipseSummaryAddress& inputAddress, bool includeIncompleteCurves ); void calculate( const std::vector<RimSummaryCase*>& summaryCases,
const RifEclipseSummaryAddress& inputAddress,
bool includeIncompleteCurves,
const std::vector<int>& percentiles = { 10, 50, 90 } );
std::vector<time_t> timeSteps( const RifEclipseSummaryAddress& resultAddress ) const override; std::vector<time_t> timeSteps( const RifEclipseSummaryAddress& resultAddress ) const override;
std::pair<bool, std::vector<double>> values( const RifEclipseSummaryAddress& resultAddress ) const override; std::pair<bool, std::vector<double>> values( const RifEclipseSummaryAddress& resultAddress ) const override;
@@ -58,11 +64,10 @@ private:
size_t keywordCount() const override; size_t keywordCount() const override;
private: private:
std::vector<time_t> m_timeSteps; std::vector<time_t> m_timeSteps;
std::vector<double> m_p10Data; std::vector<double> m_meanData;
std::vector<double> m_p50Data; std::map<int, std::vector<double>> m_percentileData;
std::vector<double> m_p90Data; std::vector<int> m_requestedPercentiles;
std::vector<double> m_meanData;
caf::PdmPointer<RimSummaryCase> m_firstSummaryCase; caf::PdmPointer<RimSummaryCase> m_firstSummaryCase;
size_t m_hash = 0; size_t m_hash = 0;
@@ -80,6 +80,7 @@ RimSummaryAddress::RimSummaryAddress()
CAF_PDM_InitField( &m_caseId, "CaseId", -1, "CaseId" ); CAF_PDM_InitField( &m_caseId, "CaseId", -1, "CaseId" );
CAF_PDM_InitField( &m_ensembleId, "EnsembleId", -1, "EnsembleId" ); CAF_PDM_InitField( &m_ensembleId, "EnsembleId", -1, "EnsembleId" );
CAF_PDM_InitField( &m_percentile, "Percentile", -1, "Percentile" );
m_category = RifEclipseSummaryAddressDefines::SummaryCategory::SUMMARY_INVALID; m_category = RifEclipseSummaryAddressDefines::SummaryCategory::SUMMARY_INVALID;
m_statistics = RifEclipseSummaryAddressDefines::StatisticsType::NONE; m_statistics = RifEclipseSummaryAddressDefines::StatisticsType::NONE;
@@ -138,6 +139,7 @@ void RimSummaryAddress::setAddress( const RifEclipseSummaryAddress& addr )
m_cellJ = addr.cellJ(); m_cellJ = addr.cellJ();
m_cellK = addr.cellK(); m_cellK = addr.cellK();
m_calculationId = addr.id(); m_calculationId = addr.id();
m_percentile = addr.percentile();
setUiName( m_vectorName ); setUiName( m_vectorName );
setUiIconFromResourceString( iconResourceText() ); setUiIconFromResourceString( iconResourceText() );
@@ -148,23 +150,25 @@ void RimSummaryAddress::setAddress( const RifEclipseSummaryAddress& addr )
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
RifEclipseSummaryAddress RimSummaryAddress::address() const RifEclipseSummaryAddress RimSummaryAddress::address() const
{ {
return RifEclipseSummaryAddress( m_category(), auto addr = RifEclipseSummaryAddress( m_category(),
m_statistics(), m_statistics(),
m_vectorName().toStdString(), m_vectorName().toStdString(),
m_regionNumber(), m_regionNumber(),
m_regionNumber2(), m_regionNumber2(),
m_groupName().toStdString(), m_groupName().toStdString(),
m_networkName().toStdString(), m_networkName().toStdString(),
m_wellName().toStdString(), m_wellName().toStdString(),
m_wellSegmentNumber(), m_wellSegmentNumber(),
m_lgrName().toStdString(), m_lgrName().toStdString(),
m_cellI(), m_cellI(),
m_cellJ(), m_cellJ(),
m_cellK(), m_cellK(),
m_aquiferNumber, m_aquiferNumber,
m_wellCompletionNumber(), m_wellCompletionNumber(),
m_isErrorResult, m_isErrorResult,
m_calculationId ); m_calculationId );
addr.setPercentile( m_percentile );
return addr;
} }
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
@@ -85,4 +85,5 @@ private:
caf::PdmField<int> m_calculationId; caf::PdmField<int> m_calculationId;
caf::PdmField<int> m_caseId; caf::PdmField<int> m_caseId;
caf::PdmField<int> m_ensembleId; caf::PdmField<int> m_ensembleId;
caf::PdmField<int> m_percentile;
}; };
@@ -1088,6 +1088,15 @@ bool RimEnsembleWellLogCurveSet::hasMeanData() const
return m_ensembleWellLogStatistics->hasMeanData(); return m_ensembleWellLogStatistics->hasMeanData();
} }
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RimEnsembleWellLogCurveSet::hasPercentileData( int /*p*/ ) const
{
// Custom percentiles are not supported for well log statistics
return false;
}
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
/// ///
//-------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------
@@ -108,6 +108,7 @@ public:
bool hasP50Data() const override; bool hasP50Data() const override;
bool hasP90Data() const override; bool hasP90Data() const override;
bool hasMeanData() const override; bool hasMeanData() const override;
bool hasPercentileData( int p ) const override;
void appendColorGroup( caf::PdmUiOrdering& uiOrdering ); void appendColorGroup( caf::PdmUiOrdering& uiOrdering );