Files
ResInsight/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryEnsembleTools.cpp
T
Magne Sjaastad f0d6081448 #14423 Add cycle-safe delta ensemble dependency traversal
Delta ensembles were located by scanning the summary case main collection for objects referring to a given ensemble, and the dependent ensembles were visited by unguarded recursion. A dependency cycle, which is constructible through the UI, made that recursion run forever, and an ensemble used as both source 1 and source 2 was reported twice.

Add dependentDeltaEnsembles(), deltaEnsemblesInUpdateOrder() and wouldCreateDependencyCycle() to RimSummaryEnsembleTools. The traversal uses the PDM back references, deduplicates, is iterative with visited and on-path sets, and returns the delta ensembles in topological order so a delta ensemble is always visited before the delta ensembles using it as a source. Back edges are logged instead of traversed.

Reimplement updateDependentDeltaEnsembles on top of the new traversal and replace RimDeltaSummaryEnsemble::findReferringEnsembles() with dependentDeltaEnsembles() at its four call sites. Back references also find a delta ensemble that is detached from the project tree, which the previous ancestor scan did not.
2026-08-14 10:23:34 +02:00

663 lines
24 KiB
C++

/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2024- 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimSummaryEnsembleTools.h"
#include "RiaLogging.h"
#include "Summary/RiaSummaryTools.h"
#include "RifReaderRftInterface.h"
#include "RifSummaryReaderInterface.h"
#include "RigEnsembleParameter.h"
#include "RimCorrelationPlotCollection.h"
#include "RimDeltaSummaryEnsemble.h"
#include "RimEnsembleCurveSet.h"
#include "RimMainPlotCollection.h"
#include "RimParameterResultCrossPlot.h"
#include "RimParameterRftCrossPlot.h"
#include "RimSummaryCase.h"
#include "RimSummaryCurve.h"
#include "RimSummaryMultiPlot.h"
#include "RimSummaryMultiPlotCollection.h"
#include "RimSummaryPlot.h"
#include "RiuPlotMainWindow.h"
#include "RiuQwtPlotWidget.h"
#include "cafPdmUiTreeView.h"
#include <algorithm>
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::set<QString> RimSummaryEnsembleTools::wellsWithRftData( const std::vector<RimSummaryCase*>& summaryCases )
{
std::set<QString> allWellNames;
for ( auto summaryCase : summaryCases )
{
if ( auto reader = summaryCase->rftReader() )
{
std::set<QString> wellNames = reader->wellNames();
allWellNames.insert( wellNames.begin(), wellNames.end() );
}
}
return allWellNames;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RigEnsembleParameter RimSummaryEnsembleTools::createEnsembleParameter( const std::vector<RimSummaryCase*>& summaryCases,
const QString& paramName )
{
RigEnsembleParameter eParam;
eParam.name = paramName;
size_t numericValuesCount = 0;
size_t textValuesCount = 0;
// Make sure the values list exactly matches the case count
// And use an invalid value (infinity) for invalid cases.
eParam.values.resize( summaryCases.size(), std::numeric_limits<double>::infinity() );
// Prepare case realization params, and check types
for ( size_t caseIdx = 0; caseIdx < summaryCases.size(); ++caseIdx )
{
auto rimCase = summaryCases[caseIdx];
auto crp = rimCase->caseRealizationParameters();
if ( !crp ) continue;
auto value = crp->parameterValue( paramName );
if ( !value.isValid() ) continue;
if ( value.isNumeric() )
{
double numVal = value.numericValue();
eParam.values[caseIdx] = QVariant( numVal );
if ( numVal < eParam.minValue ) eParam.minValue = numVal;
if ( numVal > eParam.maxValue ) eParam.maxValue = numVal;
numericValuesCount++;
}
else if ( value.isText() )
{
eParam.values[caseIdx] = QVariant( value.textValue() );
textValuesCount++;
}
}
if ( numericValuesCount && !textValuesCount )
{
eParam.type = RigEnsembleParameter::TYPE_NUMERIC;
}
else if ( textValuesCount && !numericValuesCount )
{
eParam.type = RigEnsembleParameter::TYPE_TEXT;
}
if ( numericValuesCount && textValuesCount )
{
// A mix of types have been added to parameter values
if ( numericValuesCount > textValuesCount )
{
// Use numeric type
for ( auto& val : eParam.values )
{
if ( val.type() == QVariant::String )
{
val.setValue( std::numeric_limits<double>::infinity() );
}
}
eParam.type = RigEnsembleParameter::TYPE_NUMERIC;
}
else
{
// Use text type
for ( auto& val : eParam.values )
{
if ( val.type() == QVariant::Double )
{
val.setValue( QString::number( val.value<double>() ) );
}
}
eParam.type = RigEnsembleParameter::TYPE_TEXT;
eParam.minValue = std::numeric_limits<double>::infinity();
eParam.maxValue = -std::numeric_limits<double>::infinity();
}
}
if ( eParam.isText() )
{
// Remove duplicate texts
std::set<QString> valueSet;
for ( const auto& val : eParam.values )
{
valueSet.insert( val.toString() );
}
eParam.values.clear();
for ( const auto& val : valueSet )
{
eParam.values.push_back( QVariant( val ) );
}
}
return eParam;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimSummaryEnsembleTools::sortByBinnedVariation( std::vector<RigEnsembleParameter>& parameterVector )
{
double minStdDev = std::numeric_limits<double>::infinity();
double maxStdDev = 0.0;
for ( const auto& paramPair : parameterVector )
{
double stdDev = paramPair.normalizedStdDeviation();
if ( stdDev != 0.0 )
{
minStdDev = std::min( minStdDev, stdDev );
maxStdDev = std::max( maxStdDev, stdDev );
}
}
if ( ( maxStdDev - minStdDev ) <= 0.0 )
{
return;
}
double delta = ( maxStdDev - minStdDev ) / (float)( RigEnsembleParameter::NR_OF_VARIATION_BINS );
std::vector<double> bins;
bins.push_back( 0.0 );
for ( int i = 0; i < RigEnsembleParameter::NR_OF_VARIATION_BINS - 1; ++i )
{
bins.push_back( minStdDev + ( i + 1 ) * delta );
}
for ( RigEnsembleParameter& nameParamPair : parameterVector )
{
int binNumber = -1;
for ( double bin : bins )
{
if ( nameParamPair.normalizedStdDeviation() > bin )
{
binNumber++;
}
}
nameParamPair.variationBin = binNumber;
}
// Sort by variation bin (highest first) but keep name as sorting parameter when parameters have the same variation
// index
std::stable_sort( parameterVector.begin(),
parameterVector.end(),
[]( const RigEnsembleParameter& lhs, const RigEnsembleParameter& rhs ) { return lhs.variationBin > rhs.variationBin; } );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<RigEnsembleParameter> RimSummaryEnsembleTools::alphabeticEnsembleParameters( const std::vector<RimSummaryCase*>& summaryCases )
{
std::set<QString> paramSet;
for ( RimSummaryCase* rimCase : summaryCases )
{
if ( rimCase->caseRealizationParameters() != nullptr )
{
auto ps = rimCase->caseRealizationParameters()->parameters();
for ( auto p : ps )
{
paramSet.insert( p.first );
}
}
}
std::vector<RigEnsembleParameter> sortedEnsembleParameters;
sortedEnsembleParameters.reserve( paramSet.size() );
for ( const QString& parameterName : paramSet )
{
sortedEnsembleParameters.push_back( RimSummaryEnsembleTools::createEnsembleParameter( summaryCases, parameterName ) );
}
return sortedEnsembleParameters;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<RigEnsembleParameter>
RimSummaryEnsembleTools::createVariationSortedEnsembleParameters( const std::vector<RimSummaryCase*>& summaryCases )
{
std::set<QString> paramSet;
for ( RimSummaryCase* rimCase : summaryCases )
{
if ( rimCase->caseRealizationParameters() != nullptr )
{
auto ps = rimCase->caseRealizationParameters()->parameters();
for ( const auto& p : ps )
{
paramSet.insert( p.first );
}
}
}
std::vector<RigEnsembleParameter> parameters;
parameters.reserve( paramSet.size() );
for ( const QString& parameterName : paramSet )
{
auto ensembleParameter = RimSummaryEnsembleTools::createEnsembleParameter( summaryCases, parameterName );
parameters.push_back( ensembleParameter );
}
RimSummaryEnsembleTools::sortByBinnedVariation( parameters );
return parameters;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void clearEnsembleParametersHashes( const std::vector<RimSummaryCase*>& summaryCases )
{
for ( auto sumCase : summaryCases )
{
auto crp = sumCase->caseRealizationParameters();
if ( crp ) crp->clearParametersHash();
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
size_t RimSummaryEnsembleTools::calculateEnsembleParametersIntersectionHash( const std::vector<RimSummaryCase*>& summaryCases )
{
clearEnsembleParametersHashes( summaryCases );
// Find ensemble parameters intersection
std::set<QString> paramNames;
for ( size_t i = 0; i < summaryCases.size(); i++ )
{
auto crp = summaryCases[i]->caseRealizationParameters();
if ( !crp ) continue;
auto caseParamNames = crp->parameterNames();
if ( i == 0 )
paramNames = caseParamNames;
else
{
std::set<QString> newIntersection;
std::set_intersection( paramNames.begin(),
paramNames.end(),
caseParamNames.begin(),
caseParamNames.end(),
std::inserter( newIntersection, newIntersection.end() ) );
if ( paramNames.size() != newIntersection.size() ) paramNames = newIntersection;
}
}
for ( auto sumCase : summaryCases )
{
auto crp = sumCase->caseRealizationParameters();
if ( crp ) crp->calculateParametersHash( paramNames );
}
size_t commonAddressCount = 0;
// Find common address count
for ( const auto sumCase : summaryCases )
{
const auto reader = sumCase->summaryReader();
if ( !reader ) continue;
auto currAddrCount = reader->allResultAddresses().size();
if ( commonAddressCount == 0 )
{
commonAddressCount = currAddrCount;
}
else
{
if ( currAddrCount != commonAddressCount )
{
commonAddressCount = 0;
break;
}
}
}
return commonAddressCount;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RimSummaryEnsembleTools::isEnsembleCurve( RimPlotCurve* sourceCurve )
{
auto summaryCurve = dynamic_cast<RimSummaryCurve*>( sourceCurve );
if ( !summaryCurve ) return false;
return summaryCurve->isEnsembleCurve();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<RimSummaryCase*> RimSummaryEnsembleTools::summaryCasesFromCurves( const std::vector<RimPlotCurve*>& sourceCurves )
{
std::vector<RimSummaryCase*> sourceCases;
for ( auto sourceCurve : sourceCurves )
{
auto sourceSummaryCurve = dynamic_cast<RimSummaryCurve*>( sourceCurve );
if ( !sourceSummaryCurve ) continue;
auto ensembleCurveSet = sourceSummaryCurve->firstAncestorOfType<RimEnsembleCurveSet>();
if ( !ensembleCurveSet ) continue;
auto sourceCase = sourceSummaryCurve->summaryCaseY();
if ( !sourceCase ) continue;
sourceCases.push_back( sourceCase );
}
return sourceCases;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimSummaryEnsembleTools::selectSummaryCasesInProjectTree( const std::vector<RimSummaryCase*>& sourceCases )
{
if ( sourceCases.empty() ) return;
if ( auto mainWindow = RiuPlotMainWindow::instance() )
{
if ( auto treeView = mainWindow->getTreeViewWithItem( sourceCases.front() ) )
{
std::vector<const caf::PdmUiItem*> sourceItems;
for ( auto sourceCase : sourceCases )
{
sourceItems.push_back( sourceCase );
}
treeView->selectItems( sourceItems );
}
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimSummaryEnsembleTools::highlightCurvesForSummaryCases( const std::vector<RimSummaryCase*>& sourceCases )
{
auto summaryPlotColl = RiaSummaryTools::summaryMultiPlotCollection();
for ( auto multiPlot : summaryPlotColl->multiPlots() )
{
for ( auto plot : multiPlot->summaryPlots() )
{
auto plotWidget = dynamic_cast<RiuQwtPlotWidget*>( plot->plotWidget() );
if ( !plotWidget ) continue;
// Do nothing if no curve sets are present, as this will cause the single curve highlight to be removed
if ( plot->curveSets().empty() ) continue;
std::vector<RimPlotCurve*> curvesForSameRealization;
std::vector<RimPlotCurve*> curvesInScope;
for ( const auto& curveSet : plot->curveSets() )
{
std::vector<RimPlotCurve*> curvesInCurveSet;
bool curveSetContainsSourceCase = false;
for ( auto ensembleCurve : curveSet->curves() )
{
curvesInCurveSet.push_back( ensembleCurve );
auto summaryCaseY = ensembleCurve->summaryCaseY();
if ( std::find( sourceCases.begin(), sourceCases.end(), summaryCaseY ) != sourceCases.end() )
{
curvesForSameRealization.push_back( ensembleCurve );
curveSetContainsSourceCase = true;
}
}
// Only curve sets containing a selected case participate in dimming, so curves from other ensembles are left unchanged.
if ( curveSetContainsSourceCase )
{
curvesInScope.insert( curvesInScope.end(), curvesInCurveSet.begin(), curvesInCurveSet.end() );
}
}
bool updateCurveOrder = false;
plotWidget->resetPlotItemHighlighting( updateCurveOrder );
if ( !curvesForSameRealization.empty() )
{
std::sort( curvesForSameRealization.begin(), curvesForSameRealization.end() );
curvesForSameRealization.erase( std::unique( curvesForSameRealization.begin(), curvesForSameRealization.end() ),
curvesForSameRealization.end() );
plotWidget->highlightCurvesUpdateOrder( curvesForSameRealization, curvesInScope );
}
plotWidget->replot();
}
}
auto plotColl = RimMainPlotCollection::current();
if ( auto correlationPlotColl = plotColl->correlationPlotCollection() )
{
auto crossPlots = correlationPlotColl->descendantsIncludingThisOfType<RimParameterResultCrossPlot>();
for ( auto c : crossPlots )
{
c->loadDataAndUpdate();
}
auto rftCrossPlots = correlationPlotColl->descendantsIncludingThisOfType<RimParameterRftCrossPlot>();
for ( auto c : rftCrossPlots )
{
c->loadDataAndUpdate();
}
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimSummaryEnsembleTools::resetHighlightAllPlots()
{
auto summaryPlotColl = RiaSummaryTools::summaryMultiPlotCollection();
for ( auto multiPlot : summaryPlotColl->multiPlots() )
{
for ( auto plot : multiPlot->summaryPlots() )
{
if ( auto plotWidget = dynamic_cast<RiuQwtPlotWidget*>( plot->plotWidget() ) )
{
plotWidget->resetPlotItemHighlighting();
plotWidget->replot();
}
}
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimSummaryCase* RimSummaryEnsembleTools::caseWithMostDataObjects( const std::vector<RimSummaryCase*>& sourceCases )
{
size_t maxAddrCount = 0;
size_t maxAddrIndex = sourceCases.size();
for ( size_t i = 0; i < sourceCases.size(); i++ )
{
auto currCase = sourceCases[i];
if ( !currCase ) continue;
auto reader = currCase->summaryReader();
if ( !reader ) continue;
// If no RifSummaryAddress is created, the number of keywords on file is returned. This can be the case for a realization in an
// ensemble.
auto addrCount = reader->dataObjectCount();
if ( addrCount > maxAddrCount )
{
maxAddrCount = addrCount;
maxAddrIndex = i;
}
}
if ( maxAddrIndex < sourceCases.size() )
{
return sourceCases[maxAddrIndex];
}
return nullptr;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<RimDeltaSummaryEnsemble*> RimSummaryEnsembleTools::dependentDeltaEnsembles( const RimSummaryEnsemble* sourceEnsemble )
{
std::vector<RimDeltaSummaryEnsemble*> dependents;
if ( !sourceEnsemble ) return dependents;
// One entry is returned per referring field, so a delta ensemble using the same ensemble as both sources is
// reported twice. Report each delta ensemble once.
std::set<RimDeltaSummaryEnsemble*> seen;
for ( auto deltaEnsemble : sourceEnsemble->objectsWithReferringPtrFieldsOfType<RimDeltaSummaryEnsemble>() )
{
if ( !deltaEnsemble ) continue;
if ( seen.insert( deltaEnsemble ).second ) dependents.push_back( deltaEnsemble );
}
return dependents;
}
namespace
{
//--------------------------------------------------------------------------------------------------
/// Depth first traversal of the dependency graph, following the edges from a source ensemble to the delta ensembles
/// referring to it. The reverse postorder of that traversal is a topological order, so a delta ensemble is always
/// visited before the delta ensembles using it as a source.
//--------------------------------------------------------------------------------------------------
std::vector<RimDeltaSummaryEnsemble*> dependentDeltaEnsemblesInOrder( const std::vector<const RimSummaryEnsemble*>& sourceEnsembles )
{
struct StackEntry
{
RimDeltaSummaryEnsemble* ensemble = nullptr;
std::vector<RimDeltaSummaryEnsemble*> dependents;
size_t nextDependent = 0;
};
std::vector<RimDeltaSummaryEnsemble*> postOrder;
std::set<const RimSummaryEnsemble*> visited;
std::set<const RimSummaryEnsemble*> onPath;
std::vector<StackEntry> stack;
auto pushEnsemble = [&]( RimDeltaSummaryEnsemble* deltaEnsemble )
{
if ( visited.count( deltaEnsemble ) ) return;
if ( onPath.count( deltaEnsemble ) )
{
RiaLogging::error( QString( "Delta ensemble '%1' is part of a circular dependency. The cycle is not traversed." )
.arg( deltaEnsemble->name() )
.toStdString() );
return;
}
onPath.insert( deltaEnsemble );
stack.push_back( { deltaEnsemble, RimSummaryEnsembleTools::dependentDeltaEnsembles( deltaEnsemble ), 0 } );
};
for ( auto sourceEnsemble : sourceEnsembles )
{
for ( auto deltaEnsemble : RimSummaryEnsembleTools::dependentDeltaEnsembles( sourceEnsemble ) )
{
pushEnsemble( deltaEnsemble );
while ( !stack.empty() )
{
auto& top = stack.back();
if ( top.nextDependent < top.dependents.size() )
{
auto* dependent = top.dependents[top.nextDependent++];
pushEnsemble( dependent );
}
else
{
auto* completed = top.ensemble;
stack.pop_back();
onPath.erase( completed );
visited.insert( completed );
postOrder.push_back( completed );
}
}
}
}
std::reverse( postOrder.begin(), postOrder.end() );
return postOrder;
}
} // namespace
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<RimDeltaSummaryEnsemble*>
RimSummaryEnsembleTools::deltaEnsemblesInUpdateOrder( const std::vector<RimSummaryEnsemble*>& sourceEnsembles )
{
return dependentDeltaEnsemblesInOrder( std::vector<const RimSummaryEnsemble*>( sourceEnsembles.begin(), sourceEnsembles.end() ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RimSummaryEnsembleTools::wouldCreateDependencyCycle( const RimDeltaSummaryEnsemble* deltaEnsemble,
const RimSummaryEnsemble* candidateSource )
{
if ( !deltaEnsemble || !candidateSource ) return false;
if ( deltaEnsemble == candidateSource ) return true;
// Everything depending on deltaEnsemble, directly or indirectly, would close a cycle if used as a source for it
for ( auto dependent : dependentDeltaEnsemblesInOrder( { deltaEnsemble } ) )
{
if ( dependent == candidateSource ) return true;
}
return false;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimSummaryEnsembleTools::updateDependentDeltaEnsembles( const RimSummaryEnsemble* sourceEnsemble )
{
if ( !sourceEnsemble ) return;
for ( auto deltaEnsemble : dependentDeltaEnsemblesInOrder( { sourceEnsemble } ) )
{
deltaEnsemble->onSourceEnsembleChanged();
}
}