///////////////////////////////////////////////////////////////////////////////// // // 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 // 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 //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::set RimSummaryEnsembleTools::wellsWithRftData( const std::vector& summaryCases ) { std::set allWellNames; for ( auto summaryCase : summaryCases ) { if ( auto reader = summaryCase->rftReader() ) { std::set wellNames = reader->wellNames(); allWellNames.insert( wellNames.begin(), wellNames.end() ); } } return allWellNames; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RigEnsembleParameter RimSummaryEnsembleTools::createEnsembleParameter( const std::vector& 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::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::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() ) ); } } eParam.type = RigEnsembleParameter::TYPE_TEXT; eParam.minValue = std::numeric_limits::infinity(); eParam.maxValue = -std::numeric_limits::infinity(); } } if ( eParam.isText() ) { // Remove duplicate texts std::set 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& parameterVector ) { double minStdDev = std::numeric_limits::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 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 RimSummaryEnsembleTools::alphabeticEnsembleParameters( const std::vector& summaryCases ) { std::set paramSet; for ( RimSummaryCase* rimCase : summaryCases ) { if ( rimCase->caseRealizationParameters() != nullptr ) { auto ps = rimCase->caseRealizationParameters()->parameters(); for ( auto p : ps ) { paramSet.insert( p.first ); } } } std::vector sortedEnsembleParameters; sortedEnsembleParameters.reserve( paramSet.size() ); for ( const QString& parameterName : paramSet ) { sortedEnsembleParameters.push_back( RimSummaryEnsembleTools::createEnsembleParameter( summaryCases, parameterName ) ); } return sortedEnsembleParameters; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::vector RimSummaryEnsembleTools::createVariationSortedEnsembleParameters( const std::vector& summaryCases ) { std::set 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 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& summaryCases ) { for ( auto sumCase : summaryCases ) { auto crp = sumCase->caseRealizationParameters(); if ( crp ) crp->clearParametersHash(); } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- size_t RimSummaryEnsembleTools::calculateEnsembleParametersIntersectionHash( const std::vector& summaryCases ) { clearEnsembleParametersHashes( summaryCases ); // Find ensemble parameters intersection std::set 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 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( sourceCurve ); if ( !summaryCurve ) return false; return summaryCurve->isEnsembleCurve(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::vector RimSummaryEnsembleTools::summaryCasesFromCurves( const std::vector& sourceCurves ) { std::vector sourceCases; for ( auto sourceCurve : sourceCurves ) { auto sourceSummaryCurve = dynamic_cast( sourceCurve ); if ( !sourceSummaryCurve ) continue; auto ensembleCurveSet = sourceSummaryCurve->firstAncestorOfType(); if ( !ensembleCurveSet ) continue; auto sourceCase = sourceSummaryCurve->summaryCaseY(); if ( !sourceCase ) continue; sourceCases.push_back( sourceCase ); } return sourceCases; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSummaryEnsembleTools::selectSummaryCasesInProjectTree( const std::vector& sourceCases ) { if ( sourceCases.empty() ) return; if ( auto mainWindow = RiuPlotMainWindow::instance() ) { if ( auto treeView = mainWindow->getTreeViewWithItem( sourceCases.front() ) ) { std::vector sourceItems; for ( auto sourceCase : sourceCases ) { sourceItems.push_back( sourceCase ); } treeView->selectItems( sourceItems ); } } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSummaryEnsembleTools::highlightCurvesForSummaryCases( const std::vector& sourceCases ) { auto summaryPlotColl = RiaSummaryTools::summaryMultiPlotCollection(); for ( auto multiPlot : summaryPlotColl->multiPlots() ) { for ( auto plot : multiPlot->summaryPlots() ) { auto plotWidget = dynamic_cast( 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 curvesForSameRealization; std::vector curvesInScope; for ( const auto& curveSet : plot->curveSets() ) { std::vector 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(); for ( auto c : crossPlots ) { c->loadDataAndUpdate(); } auto rftCrossPlots = correlationPlotColl->descendantsIncludingThisOfType(); 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( plot->plotWidget() ) ) { plotWidget->resetPlotItemHighlighting(); plotWidget->replot(); } } } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RimSummaryCase* RimSummaryEnsembleTools::caseWithMostDataObjects( const std::vector& 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 RimSummaryEnsembleTools::dependentDeltaEnsembles( const RimSummaryEnsemble* sourceEnsemble ) { std::vector 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 seen; for ( auto deltaEnsemble : sourceEnsemble->objectsWithReferringPtrFieldsOfType() ) { 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 dependentDeltaEnsemblesInOrder( const std::vector& sourceEnsembles ) { struct StackEntry { RimDeltaSummaryEnsemble* ensemble = nullptr; std::vector dependents; size_t nextDependent = 0; }; std::vector postOrder; std::set visited; std::set onPath; std::vector 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 RimSummaryEnsembleTools::deltaEnsemblesInUpdateOrder( const std::vector& sourceEnsembles ) { return dependentDeltaEnsemblesInOrder( std::vector( 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(); } }